mirror of
https://github.com/Xahau/xahau.js.git
synced 2025-11-15 18:15:49 +00:00
Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4f9be5af8 | ||
|
|
473d8a8d8c | ||
|
|
41ea820ae0 | ||
|
|
0558ad689a | ||
|
|
3199aa438a | ||
|
|
c3f630c27f | ||
|
|
cf3a21a712 | ||
|
|
d8504a3001 | ||
|
|
a2b07d5edd | ||
|
|
13a6a2c335 | ||
|
|
e19be192bd | ||
|
|
c32216c9e5 | ||
|
|
904082a86c | ||
|
|
f56a20d697 | ||
|
|
8275e036c9 | ||
|
|
903e480130 | ||
|
|
30fd0e7bff | ||
|
|
fbdef6eea0 | ||
|
|
5a04ce9629 | ||
|
|
693e2aaae7 | ||
|
|
43deeaf5fb | ||
|
|
cbba7727f2 | ||
|
|
52e1665e72 | ||
|
|
66ea770287 | ||
|
|
18efa5d742 | ||
|
|
802212bbdc | ||
|
|
7f59fb917c | ||
|
|
6ebaec31a5 | ||
|
|
14f409ff56 | ||
|
|
8ffd0b13a3 | ||
|
|
969873441e | ||
|
|
282ac6d8ab | ||
|
|
1e3c96b14f | ||
|
|
b14fab8aa7 | ||
|
|
be33b1be60 | ||
|
|
06288e798e | ||
|
|
0de7d84862 | ||
|
|
58afce517a | ||
|
|
250e987fd9 | ||
|
|
87ba2abc9a | ||
|
|
716fd0b938 | ||
|
|
893fc4c168 | ||
|
|
6f5cf8506f | ||
|
|
c808cb0a1c | ||
|
|
5f677a86a7 | ||
|
|
11540f8cd9 | ||
|
|
9d6ccdcab1 |
@@ -36,12 +36,15 @@ 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",
|
||||
"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-ecdsa-recoverablepublickey.js",
|
||||
"src/js/sjcl-custom/sjcl-jacobi.js"
|
||||
],
|
||||
dest: 'build/sjcl.js'
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#The Ripple JavaScript Library
|
||||
|
||||
[](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**
|
||||
###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
|
||||
|
||||
473
build/sjcl.js
473
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,37 +4104,63 @@ 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) {
|
||||
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));
|
||||
};
|
||||
@@ -4078,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);
|
||||
|
||||
@@ -52,7 +52,7 @@ A new `Remote` can be created with the following options:
|
||||
, secure: <boolean>
|
||||
}
|
||||
```
|
||||
|
||||
+ `local_signing`
|
||||
|
||||
#2. `Remote` functions
|
||||
|
||||
@@ -60,7 +60,7 @@ A new `Remote` can be created with the following options:
|
||||
|
||||
##Server info functions
|
||||
|
||||
**[request_server_info([callback])](https://ripple.com/wiki/RPC_API#server_info)**
|
||||
**[requestServerInfo([callback])](https://ripple.com/wiki/RPC_API#server_info)**
|
||||
|
||||
Returns information about the state of the server. If you are connected to multiple servers and want to select by a particular host, use `request.set_server`. Example:
|
||||
|
||||
@@ -73,34 +73,34 @@ request.callback(function(err, res) {
|
||||
request.request();
|
||||
```
|
||||
|
||||
**[request_unl_list([callback])](https://ripple.com/wiki/RPC_API#unl_list)**
|
||||
**[requestUnlList([callback])](https://ripple.com/wiki/RPC_API#unl_list)**
|
||||
|
||||
**[request_unl_add(addr, comment, [callback])](https://ripple.com/wiki/RPC_API#unl_add)**
|
||||
**[requestUnlAdd(addr, comment, [callback])](https://ripple.com/wiki/RPC_API#unl_add)**
|
||||
|
||||
**[request_unl_delete(node, [callback])](https://ripple.com/wiki/RPC_API#unl_delete)**
|
||||
**[requestUnlDelete(node, [callback])](https://ripple.com/wiki/RPC_API#unl_delete)**
|
||||
|
||||
**[request_peers([callback])](https://ripple.com/wiki/RPC_API#peers)**
|
||||
**[requestPeers([callback])](https://ripple.com/wiki/RPC_API#peers)**
|
||||
|
||||
|
||||
**[request_connect(ip, port, [callback])](https://ripple.com/wiki/RPC_API#connect)**
|
||||
**[requestConnect(ip, port, [callback])](https://ripple.com/wiki/RPC_API#connect)**
|
||||
|
||||
|
||||
|
||||
##Ledger query functions
|
||||
|
||||
**[request_ledger(ledger, [opts], [callback])](https://ripple.com/wiki/RPC_API#ledger)**
|
||||
**[requestLedger(ledger, [opts], [callback])](https://ripple.com/wiki/RPC_API#ledger)**
|
||||
|
||||
**request_ledger_header([callback])**
|
||||
**requestLedgerHeader([callback])**
|
||||
|
||||
**[request_ledger_current([callback])](https://ripple.com/wiki/RPC_API#ledger_current)**
|
||||
**[requestLedgerCurrent([callback])](https://ripple.com/wiki/RPC_API#ledger_current)**
|
||||
|
||||
**[request_ledger_entry(type, [callback])](https://ripple.com/wiki/RPC_API#ledger_entry)**
|
||||
**[requestLedgerEntry(type, [callback])](https://ripple.com/wiki/RPC_API#ledger_entry)**
|
||||
|
||||
**[request_subscribe(streams, [callback])](https://ripple.com/wiki/RPC_API#subscribe)**
|
||||
**[requestSubscribe(streams, [callback])](https://ripple.com/wiki/RPC_API#subscribe)**
|
||||
|
||||
Start receiving selected streams from the server.
|
||||
|
||||
**[request_unsubscribe(streams, [callback])](https://ripple.com/wiki/RPC_API#unsubscribe)**
|
||||
**[requestUnsubscribe(streams, [callback])](https://ripple.com/wiki/RPC_API#unsubscribe)**
|
||||
|
||||
Stop receiving selected streams from the server.
|
||||
|
||||
@@ -109,11 +109,11 @@ Stop receiving selected streams from the server.
|
||||
|
||||
##Transaction query functions
|
||||
|
||||
**[request_transaction_entry(hash, [ledger_hash], [callback])](https://ripple.com/wiki/RPC_API#transaction_entry)**
|
||||
**[requestTransactionEntry(hash, [ledger_hash], [callback])](https://ripple.com/wiki/RPC_API#transaction_entry)**
|
||||
|
||||
Searches a particular ledger for a transaction hash. Default ledger is the open ledger.
|
||||
|
||||
**[request_tx(hash, [callback])](https://ripple.com/wiki/RPC_API#tx)**
|
||||
**[requestTx(hash, [callback])](https://ripple.com/wiki/RPC_API#tx)**
|
||||
|
||||
Searches ledger history for validated transaction hashes.
|
||||
|
||||
@@ -122,7 +122,7 @@ Searches ledger history for validated transaction hashes.
|
||||
|
||||
##Account query functions
|
||||
|
||||
**[request_account_info(account, [callback])](https://ripple.com/wiki/RPC_API#account_info)**
|
||||
**[requestAccountInfo(account, [callback])](https://ripple.com/wiki/RPC_API#account_info)**
|
||||
|
||||
Return information about the specified account.
|
||||
|
||||
@@ -143,13 +143,13 @@ Return information about the specified account.
|
||||
}
|
||||
```
|
||||
|
||||
**[request_account_lines(accountID, account_index, current, [callback])](https://ripple.com/wiki/RPC_API#account_lines)**
|
||||
**[requestAccountLines(accountID, account_index, current, [callback])](https://ripple.com/wiki/RPC_API#account_lines)**
|
||||
|
||||
**[request_account_offers(accountID, account_index, current, [callback])](https://ripple.com/wiki/RPC_API#account_offers)**
|
||||
**[requestAccountOffers(accountID, account_index, current, [callback])](https://ripple.com/wiki/RPC_API#account_offers)**
|
||||
|
||||
Return the specified account's outstanding offers.
|
||||
|
||||
**[request_account_tx(opts, [callback])](https://ripple.com/wiki/RPC_API#account_tx)**
|
||||
**[requestAccountTx(opts, [callback])](https://ripple.com/wiki/RPC_API#account_tx)**
|
||||
|
||||
Fetch a list of transactions that applied to this account.
|
||||
|
||||
@@ -167,25 +167,25 @@ Options:
|
||||
+ `fwd_marker`
|
||||
+ `rev_marker`
|
||||
|
||||
**[request_wallet_accounts(seed, [callback])](https://ripple.com/wiki/RPC_API#wallet_accounts)**
|
||||
**[requestWalletAccounts(seed, [callback])](https://ripple.com/wiki/RPC_API#wallet_accounts)**
|
||||
|
||||
Return a list of accounts for a wallet.
|
||||
|
||||
+ requires trusted remote
|
||||
|
||||
**request_account_balance(account, ledger, [callback])**
|
||||
**requestAccountBalance(account, ledger, [callback])**
|
||||
|
||||
Get the balance for an account. Returns an [Amount](https://github.com/ripple/ripple-lib/blob/develop/src/js/ripple/amount.js) object.
|
||||
|
||||
**request_account_flags(account, current, [callback])**
|
||||
**requestAccountFlags(account, current, [callback])**
|
||||
|
||||
Return the flags for an account.
|
||||
|
||||
**request_owner_count(account, current, [callback])**
|
||||
**requestOwnerCount(account, current, [callback])**
|
||||
|
||||
Return the owner count for an account.
|
||||
|
||||
**request_ripple_balance(account, issuer, currency, current, [callback])**
|
||||
**requestRippleBalance(account, issuer, currency, current, [callback])**
|
||||
|
||||
Return a request to get a ripple balance
|
||||
|
||||
@@ -194,7 +194,7 @@ Return a request to get a ripple balance
|
||||
|
||||
##Order book query functions
|
||||
|
||||
**[request_book_offers(gets, pays, taker, [callback])](https://ripple.com/wiki/RPC_API#book_offers)**
|
||||
**[requestBookOffers(gets, pays, taker, [callback])](https://ripple.com/wiki/RPC_API#book_offers)**
|
||||
|
||||
Return the offers for an order book as one or more pages.
|
||||
|
||||
@@ -217,18 +217,18 @@ request.request();
|
||||
|
||||
##Transaction submission functions
|
||||
|
||||
**[request_sign(secret, tx_json, [callback])](https://ripple.com/wiki/RPC_API#sign)**
|
||||
**[requestSign(secret, tx_json, [callback])](https://ripple.com/wiki/RPC_API#sign)**
|
||||
|
||||
Sign a transaction.
|
||||
|
||||
+ requires trusted remote
|
||||
|
||||
**[request_submit([callback])](https://ripple.com/wiki/RPC_API#submit)**
|
||||
**[requestSubmit([callback])](https://ripple.com/wiki/RPC_API#submit)**
|
||||
|
||||
Submit a transaction to the network. This command is used internally to submit transactions with a greater degree of reliability. See [Submitting a payment to the network](GUIDES.md#3-submitting-a-payment-to-the-network) for details.
|
||||
|
||||
|
||||
**[request_ripple_path_find(src_account, dst_account, dst_amount, src_currencies, [callback])](https://ripple.com/wiki/RPC_API#path_find)**
|
||||
**[requestRipplePathFind(src_account, dst_account, dst_amount, src_currencies, [callback])](https://ripple.com/wiki/RPC_API#path_find)**
|
||||
|
||||
|
||||
**transaction([destination], [source], [amount], [callback])**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ripple-lib",
|
||||
"version": "0.7.35",
|
||||
"version": "0.7.36",
|
||||
"description": "Ripple JavaScript client library",
|
||||
"files": [
|
||||
"src/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
|
||||
|
||||
@@ -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, opts) {
|
||||
return (new Amount()).parse_quality(quality, currency, issuer, opts);
|
||||
};
|
||||
|
||||
Amount.from_human = function (j, opts) {
|
||||
@@ -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.
|
||||
@@ -349,9 +372,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 +395,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 +438,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 +458,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
|
||||
@@ -618,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 (q, c, i) {
|
||||
/**
|
||||
* 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(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(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;
|
||||
}
|
||||
|
||||
@@ -850,6 +963,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 +1031,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;
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
|
||||
203
src/js/ripple/message.js
Normal file
203
src/js/ripple/message.js
Normal file
@@ -0,0 +1,203 @@
|
||||
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 Account = require('./account').Account;
|
||||
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
|
||||
* @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, account) {
|
||||
|
||||
return Message.signHash(Message.HASH_FUNCTION(Message.MAGIC_BYTES + message), secret_key, account);
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @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, account) {
|
||||
|
||||
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(account)._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);
|
||||
}
|
||||
|
||||
if (public_key) {
|
||||
async_callback(null, public_key);
|
||||
} else {
|
||||
async_callback(new Error('Could not recover public key from signature'));
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
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 account_class_instance = new Account(remote, account);
|
||||
account_class_instance.publicKeyIsActive(public_key_hex, async_callback);
|
||||
|
||||
};
|
||||
|
||||
var steps = [
|
||||
recoverPublicKey,
|
||||
checkPublicKeyIsValid
|
||||
];
|
||||
|
||||
async.waterfall(steps, callback);
|
||||
|
||||
};
|
||||
|
||||
module.exports = Message;
|
||||
@@ -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;
|
||||
|
||||
@@ -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: <string>
|
||||
@@ -78,10 +79,11 @@ 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.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;
|
||||
@@ -239,20 +241,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 +346,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');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -529,7 +531,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;
|
||||
}
|
||||
|
||||
@@ -699,20 +701,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 +732,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 +975,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 +1257,7 @@ Remote.accountRootRequest = function(type, responseFilter, account, ledger, call
|
||||
}
|
||||
|
||||
var request = this.requestLedgerEntry('account_root');
|
||||
|
||||
request.accountRoot(account);
|
||||
request.ledgerChoose(ledger);
|
||||
|
||||
@@ -1314,7 +1313,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;
|
||||
}
|
||||
|
||||
@@ -1666,7 +1665,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);
|
||||
|
||||
@@ -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 = { };
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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;
|
||||
@@ -73,34 +74,25 @@ function Transaction(remote) {
|
||||
// Index at which transaction was submitted
|
||||
this.submitIndex = void(0);
|
||||
|
||||
// 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
|
||||
// effecting the Fee amount). This should be populated with a transactionID
|
||||
// any time it goes on the network
|
||||
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;
|
||||
}
|
||||
|
||||
self.emit('cleanup', message);
|
||||
};
|
||||
this.submittedIDs = [ ];
|
||||
|
||||
this.once('success', function(message) {
|
||||
self.finalized = true;
|
||||
self.finalize(message);
|
||||
self.setState('validated');
|
||||
finalize(message);
|
||||
self.emit('cleanup', message);
|
||||
});
|
||||
|
||||
this.once('error', function(message) {
|
||||
self.finalized = true;
|
||||
self.finalize(message);
|
||||
self.setState('failed');
|
||||
finalize(message);
|
||||
self.emit('cleanup', message);
|
||||
});
|
||||
|
||||
this.once('submitted', function() {
|
||||
@@ -120,6 +112,11 @@ Transaction.fee_units = {
|
||||
};
|
||||
|
||||
Transaction.flags = {
|
||||
// Universal flags can apply to any transaction type
|
||||
Universal: {
|
||||
FullyCanonicalSig: 0x80000000
|
||||
},
|
||||
|
||||
AccountSet: {
|
||||
RequireDestTag: 0x00010000,
|
||||
OptionalDestTag: 0x00020000,
|
||||
@@ -149,6 +146,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 = {
|
||||
@@ -205,6 +214,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];
|
||||
};
|
||||
@@ -258,9 +281,28 @@ 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._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 the Fee hasn't been set, one needs to be computed by
|
||||
@@ -272,10 +314,18 @@ 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();
|
||||
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
|
||||
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
|
||||
// operations. We need to convert it back to an unsigned int.
|
||||
this.tx_json.Flags = this.tx_json.Flags >>> 0;
|
||||
}
|
||||
|
||||
return this.tx_json;
|
||||
@@ -304,7 +354,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;
|
||||
@@ -315,6 +366,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;
|
||||
}
|
||||
|
||||
@@ -325,6 +377,8 @@ Transaction.prototype.sign = function() {
|
||||
this.tx_json.TxnSignature = hex;
|
||||
this.previousSigningHash = hash;
|
||||
|
||||
callback();
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
@@ -481,20 +535,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<l; i++) {
|
||||
var flag = flag_set[i];
|
||||
|
||||
if (transaction_flags.hasOwnProperty(flag)) {
|
||||
this.tx_json.Flags += transaction_flags[flag];
|
||||
} else {
|
||||
// XXX Immediately report an error or mark it.
|
||||
return this.emit('error', new RippleError('tejInvalidFlag'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -508,10 +558,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;
|
||||
src = options.source || options.from || options.account;
|
||||
set_flag = options.set_flag || options.set;
|
||||
clear_flag = options.clear_flag || options.clear;
|
||||
}
|
||||
|
||||
if (!UInt160.is_valid(src)) {
|
||||
@@ -520,6 +590,21 @@ Transaction.prototype.accountSet = function(src) {
|
||||
|
||||
this.tx_json.TransactionType = 'AccountSet';
|
||||
this.tx_json.Account = UInt160.json_rewrite(src);
|
||||
|
||||
var SetClearFlags = Transaction.set_clear_flags.AccountSet;
|
||||
|
||||
function prepareFlag(flag) {
|
||||
return (typeof flag === 'number') ? flag : (SetClearFlags[flag] || SetClearFlags['asf' + flag]);
|
||||
};
|
||||
|
||||
if (set_flag && (set_flag = prepareFlag(set_flag))) {
|
||||
this.tx_json.SetFlag = set_flag;
|
||||
}
|
||||
|
||||
if (clear_flag && (clear_flag = prepareFlag(clear_flag))) {
|
||||
this.tx_json.ClearFlag = clear_flag;
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
@@ -529,13 +614,14 @@ 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';
|
||||
this.tx_json.Generator = generator;
|
||||
this.tx_json.PublicKey = public_key;
|
||||
this.tx_json.Signature = signature;
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
@@ -543,7 +629,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)) {
|
||||
@@ -553,6 +639,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;
|
||||
};
|
||||
|
||||
@@ -567,7 +654,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)) {
|
||||
@@ -580,15 +667,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) {
|
||||
@@ -611,6 +690,7 @@ Transaction.prototype.passwordFund = function(src, dst) {
|
||||
|
||||
this.tx_json.TransactionType = 'PasswordFund';
|
||||
this.tx_json.Destination = UInt160.json_rewrite(dst);
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
@@ -621,7 +701,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)) {
|
||||
@@ -633,9 +713,39 @@ 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;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 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:
|
||||
@@ -658,10 +768,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 (src.invoiceID) {
|
||||
this.invoiceID(src.invoiceID);
|
||||
}
|
||||
|
||||
if (!UInt160.is_valid(src)) {
|
||||
@@ -691,7 +802,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)) {
|
||||
@@ -725,7 +836,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)) {
|
||||
@@ -737,6 +848,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;
|
||||
};
|
||||
|
||||
@@ -766,12 +878,12 @@ Transaction.prototype.submit = function(callback) {
|
||||
var account = this.tx_json.Account;
|
||||
|
||||
if (typeof account !== 'string') {
|
||||
this.emit('error', new RippleError('tejInvalidAccount', 'Account is unspecified'));
|
||||
} else {
|
||||
// YYY Might check paths for invalid accounts.
|
||||
this.remote.account(account).submit(this);
|
||||
return this.emit('error', new RippleError('tejInvalidAccount', 'Account is unspecified'));
|
||||
}
|
||||
|
||||
// YYY Might check paths for invalid accounts.
|
||||
this.remote.account(account).submit(this);
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
|
||||
@@ -184,25 +184,34 @@ 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;
|
||||
return callback(new Error('Failed to fetch account transaction sequence'));
|
||||
}
|
||||
|
||||
var sequenceDif = tx.tx_json.Sequence - sequence;
|
||||
var submitted = 0;
|
||||
|
||||
for (var i=sequence; i<tx.tx_json.Sequence; i++) {
|
||||
submitFill(i, function() {
|
||||
;(function nextFill(sequence) {
|
||||
if (sequence >= tx.tx_json.Sequence) return;
|
||||
|
||||
submitFill(sequence, function() {
|
||||
if (++submitted === sequenceDif) {
|
||||
callback();
|
||||
} else {
|
||||
nextFill(sequence + 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
})(sequence);
|
||||
};
|
||||
|
||||
this._loadSequence(sequenceLoaded);
|
||||
@@ -319,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) {
|
||||
@@ -378,6 +350,7 @@ TransactionManager.prototype._request = function(tx) {
|
||||
|
||||
function transactionRetry(message) {
|
||||
if (tx.finalized) return;
|
||||
|
||||
self._fillSequence(tx, function() {
|
||||
self._resubmit(1, tx);
|
||||
});
|
||||
@@ -453,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
|
||||
@@ -503,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;
|
||||
};
|
||||
|
||||
@@ -580,27 +597,22 @@ TransactionManager.prototype.submit = function(tx) {
|
||||
tx.tx_json.Sequence = this._nextSequence++;
|
||||
}
|
||||
|
||||
// Attach secret, associate transaction with a server, attach fee.
|
||||
// If the transaction can't complete, decrement sequence so that
|
||||
// subsequent transactions
|
||||
if (!tx.complete()) {
|
||||
this._nextSequence--;
|
||||
return;
|
||||
}
|
||||
|
||||
tx.attempts = 0;
|
||||
|
||||
// Attach secret, associate transaction with a server, attach fee
|
||||
tx.complete();
|
||||
|
||||
var fee = Number(tx.tx_json.Fee);
|
||||
|
||||
if (!tx._secret && !tx.tx_json.TxnSignature) {
|
||||
tx.emit('error', new RippleError('tejSecretUnknown', 'Missing secret'));
|
||||
} else if (!remote.trusted && !remote.local_signing) {
|
||||
tx.emit('error', new RippleError('tejServerUntrusted', 'Attempt to give secret to untrusted server'));
|
||||
} else if (fee && fee > 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;
|
||||
|
||||
@@ -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<this._queue.length; i++) {
|
||||
var submitIndex = this._queue[i].submitIndex;
|
||||
|
||||
if (typeof submitIndex !== 'number') {
|
||||
// If any pending transactions don't have a submit index,
|
||||
// return -1 for scanning all previous transactions
|
||||
minLedger = -1;
|
||||
break;
|
||||
}
|
||||
|
||||
if (submitIndex < minLedger) {
|
||||
minLedger = submitIndex;
|
||||
}
|
||||
};
|
||||
|
||||
if (!isFinite(minLedger)) minLedger = -1;
|
||||
|
||||
if (minLedger !== -1) minLedger -= 1;
|
||||
|
||||
return minLedger;
|
||||
};
|
||||
|
||||
TransactionQueue.prototype.save = function() {
|
||||
if (typeof this._save !== 'function') return;
|
||||
|
||||
@@ -60,7 +31,7 @@ TransactionQueue.prototype.save = function() {
|
||||
*/
|
||||
|
||||
TransactionQueue.prototype.addReceivedSequence = function(sequence) {
|
||||
this._sequenceCache[sequence] = true;
|
||||
this._sequenceCache.set(String(sequence), true);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -68,7 +39,7 @@ TransactionQueue.prototype.addReceivedSequence = function(sequence) {
|
||||
*/
|
||||
|
||||
TransactionQueue.prototype.addReceivedId = function(id, transaction) {
|
||||
this._idCache[id] = transaction;
|
||||
this._idCache.set(id, transaction);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -76,7 +47,7 @@ TransactionQueue.prototype.addReceivedId = function(id, transaction) {
|
||||
*/
|
||||
|
||||
TransactionQueue.prototype.getReceived = function(id) {
|
||||
return this._idCache[id];
|
||||
return this._idCache.get(id);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -85,7 +56,7 @@ TransactionQueue.prototype.getReceived = function(id) {
|
||||
*/
|
||||
|
||||
TransactionQueue.prototype.hasSequence = function(sequence) {
|
||||
return this._sequenceCache[sequence] || false;
|
||||
return this._sequenceCache.has(String(sequence));
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -121,10 +92,6 @@ TransactionQueue.prototype.remove = function(tx) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!this._queue.length) {
|
||||
this.clearCache();
|
||||
}
|
||||
|
||||
this.save();
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
83
src/js/sjcl-custom/sjcl-ecc-pointextras.js
Normal file
83
src/js/sjcl-custom/sjcl-ecc-pointextras.js
Normal file
@@ -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 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 left_hand_side.equals(right_hand_side);
|
||||
|
||||
};
|
||||
|
||||
|
||||
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() +
|
||||
')';
|
||||
};
|
||||
17
src/js/sjcl-custom/sjcl-ecdsa-canonical.js
Normal file
17
src/js/sjcl-custom/sjcl-ecdsa-canonical.js
Normal file
@@ -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));
|
||||
};
|
||||
|
||||
306
src/js/sjcl-custom/sjcl-ecdsa-recoverablepublickey.js
Normal file
306
src/js/sjcl-custom/sjcl-ecdsa-recoverablepublickey.js
Normal file
@@ -0,0 +1,306 @@
|
||||
/**
|
||||
* 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));
|
||||
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,30 +1,38 @@
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
181
test/account-test.js
Normal file
181
test/account-test.js
Normal file
@@ -0,0 +1,181 @@
|
||||
var assert = require('assert');
|
||||
var Account = require('../src/js/ripple/account').Account;
|
||||
|
||||
describe('Account', function(){
|
||||
|
||||
describe('._publicKeyToAddress()', function(){
|
||||
|
||||
it('should throw an error if the key is invalid', function(){
|
||||
try {
|
||||
Account._publicKeyToAddress('not a real key');
|
||||
} catch (e) {
|
||||
assert(e);
|
||||
}
|
||||
});
|
||||
|
||||
it('should return unchanged a valid UINT160', function(){
|
||||
assert('rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz' === Account._publicKeyToAddress('rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz'));
|
||||
});
|
||||
|
||||
it('should parse a hex-encoded public key as a UINT160', function(){
|
||||
assert('rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz' === Account._publicKeyToAddress('025B32A54BFA33FB781581F49B235C0E2820C929FF41E677ADA5D3E53CFBA46332'));
|
||||
|
||||
assert('rLpq5RcRzA8FU1yUqEPW4xfsdwon7casuM' === Account._publicKeyToAddress('03BFA879C00D58CF55F2B5975FF9B5293008FF49BEFB3EE6BEE2814247BF561A23'));
|
||||
|
||||
assert('rP4yWwjoDGF2iZSBdAQAgpC449YDezEbT1' === Account._publicKeyToAddress('02DF0AB18930B6410CA9F55CB37541F1FED891B8EDF8AB1D01D8F23018A4B204A7'));
|
||||
|
||||
assert('rLdfp6eoR948KVxfn6EpaaNTKwfwXhzSeQ' === Account._publicKeyToAddress('0310C451A40CAFFD39D6B8A3BD61BF65BCA55246E9DABC3170EBE431D30655B61F'));
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
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 account = new Account({
|
||||
on: function(){},
|
||||
request_account_info: function(address, callback) {
|
||||
if (address === 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz') {
|
||||
callback(null, { account_data: {
|
||||
Account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz',
|
||||
Flags: 65536,
|
||||
LedgerEntryType: 'AccountRoot'
|
||||
}});
|
||||
}
|
||||
}
|
||||
}, 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz');
|
||||
account.publicKeyIsActive('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 account = new Account({
|
||||
on: function(){},
|
||||
request_account_info: function(address, callback) {
|
||||
if (address === 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz') {
|
||||
callback(null, { account_data: {
|
||||
Account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz',
|
||||
Flags: parseInt(65536 | 0x00100000),
|
||||
LedgerEntryType: 'AccountRoot'
|
||||
}});
|
||||
}
|
||||
}
|
||||
}, 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz');
|
||||
account.publicKeyIsActive('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 account = new Account({
|
||||
on: function(){},
|
||||
request_account_info: function(address, callback) {
|
||||
if (address === 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz') {
|
||||
callback(null, { account_data: {
|
||||
Account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz',
|
||||
Flags: parseInt(65536 | 0x00100000),
|
||||
LedgerEntryType: 'AccountRoot',
|
||||
RegularKey: 'rNw4ozCG514KEjPs5cDrqEcdsi31Jtfm5r'
|
||||
}});
|
||||
}
|
||||
}
|
||||
}, 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz');
|
||||
account.publicKeyIsActive('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 account = new Account({
|
||||
on: function(){},
|
||||
request_account_info: function(address, callback) {
|
||||
if (address === 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz') {
|
||||
callback(null, { account_data: {
|
||||
Account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz',
|
||||
Flags: parseInt(65536 | 0x00100000),
|
||||
LedgerEntryType: 'AccountRoot',
|
||||
RegularKey: 'rNw4ozCG514KEjPs5cDrqEcdsi31Jtfm5r'
|
||||
}});
|
||||
}
|
||||
}
|
||||
}, 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz');
|
||||
account.publicKeyIsActive('032ECDA93970BC7E8872EF6582CB52A5557F117244A949EB4FA8AC7688CF24FBC8', function(err, is_valid){
|
||||
assert(err === null);
|
||||
assert(is_valid === false);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should respond false if the public key is invalid', function(){
|
||||
|
||||
var account = new Account({
|
||||
on: function(){},
|
||||
request_account_info: function(address, callback) {
|
||||
if (address === 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz') {
|
||||
callback(null, { account_data: {
|
||||
Account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz',
|
||||
Flags: parseInt(65536 | 0x00100000),
|
||||
LedgerEntryType: 'AccountRoot',
|
||||
RegularKey: 'rNw4ozCG514KEjPs5cDrqEcdsi31Jtfm5r'
|
||||
}});
|
||||
}
|
||||
}
|
||||
}, 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz');
|
||||
account.publicKeyIsActive('not a real public key', function(err, is_valid){
|
||||
assert(err);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should assume the master key is valid for unfunded accounts', function(){
|
||||
|
||||
var account = new Account({
|
||||
on: function(){},
|
||||
request_account_info: function(address, 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.'
|
||||
});
|
||||
}
|
||||
}
|
||||
}, 'rLdfp6eoR948KVxfn6EpaaNTKwfwXhzSeQ');
|
||||
account.publicKeyIsActive('0310C451A40CAFFD39D6B8A3BD61BF65BCA55246E9DABC3170EBE431D30655B61F', function(err, is_valid){
|
||||
assert(!err);
|
||||
assert(is_valid);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -431,4 +431,130 @@ 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');
|
||||
});
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
327
test/message-test.js
Normal file
327
test/message-test.js
Normal file
@@ -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();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
28
test/sjcl-ecdsa-canonical-test.js
Normal file
28
test/sjcl-ecdsa-canonical-test.js
Normal file
@@ -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
|
||||
245
test/sjcl-ecdsa-recoverablepublickey-test.js
Normal file
245
test/sjcl-ecdsa-recoverablepublickey-test.js
Normal file
@@ -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);
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
25
test/uint-test.js
Normal file
25
test/uint-test.js
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user