Merge pull request #88 from justmoon/feature/generate_ledger_entry_indexes

Generate ledger entry indexes
This commit is contained in:
wltsmrz
2014-06-03 13:41:08 -07:00
10 changed files with 380 additions and 187 deletions

View File

@@ -1,4 +1,5 @@
var crypt = require('./crypt').Crypt;
var SignedRequest = require('./signedrequest').SignedRequest;
var request = require('superagent');
var extend = require("extend");
@@ -34,7 +35,6 @@ BlobObj.ops = {
BlobObj.opsReverseMap = [ ];
for (var name in BlobObj.ops) {
BlobObj.opsReverseMap[BlobObj.ops[name]] = name;
}
@@ -153,7 +153,9 @@ BlobObj.prototype.consolidate = function(fn) {
},
};
var signed = crypt.signRequestHmac(config, this.data.auth_secret, this.id);
var signedRequest = new SignedRequest(config);
var signed = signedRequest.signHmac(this.data.auth_secret, this.id);
request.post(signed.url)
.send(signed.data)
@@ -198,8 +200,7 @@ BlobObj.prototype.applyEncryptedPatch = function(patch) {
*
* @param {string} secretUnlockkey
*/
BlobObj.prototype.encryptSecret = function(secretUnlockKey, secret) {
BlobObj.prototype.encryptSecret = function (secretUnlockKey, secret) {
return crypt.encrypt(secretUnlockKey, secret);
};
@@ -483,7 +484,10 @@ BlobObj.prototype.postUpdate = function(op, pointer, params, fn) {
}
};
var signed = crypt.signRequestHmac(config, this.data.auth_secret, this.id);
var signedRequest = new SignedRequest(config);
var signed = signedRequest.signHmac(this.data.auth_secret, this.id);
request.post(signed.url)
.send(signed.data)
@@ -781,6 +785,7 @@ exports.getRippleName = function(url, address, fn) {
return fn (new Error('Invalid ripple address'));
}
if (!crypt.isValidAddress(address)) return fn (new Error("Invalid ripple address"));
request.get(url + '/v1/user/' + address, function(err, resp){
if (err) {
fn(new Error('Unable to access vault sever'));
@@ -871,7 +876,9 @@ BlobClient.create = function(options, fn) {
}
};
var signed = crypt.signRequestAsymmetric(config, options.masterkey, blob.data.account_id, options.id);
var signedRequest = new SignedRequest(config);
var signed = signedRequest.signAsymmetric(options.masterkey, blob.data.account_id, options.id);
request.post(signed)
.send(signed.data)

View File

@@ -1,8 +1,8 @@
var sjcl = require('./utils').sjcl;
var base = require('./base').Base;
var UInt160 = require('./uint160').UInt160;
var message = require('./message');
var request = require('superagent');
var UInt256 = require('./uint256').UInt256;
var request = require('superagent');
var querystring = require('querystring');
var extend = require("extend");
var parser = require("url");
@@ -284,15 +284,28 @@ Crypt.getAddress = function (masterkey) {
};
/**
* Hash data
* Hash data using SHA-512.
*
* @param {string} data
* @param {string|bitArray} data
* @return {string} Hash of the data
*/
Crypt.hashSha512 = function (data) {
// XXX Should return a UInt512
return sjcl.codec.hex.fromBits(sjcl.hash.sha512.hash(data));
}
/**
* Hash data using SHA-512 and return the first 256 bits.
*
* @param {string|bitArray} data
* @return {UInt256} Hash of the data
*/
Crypt.hashSha512Half = function (data) {
return UInt256.from_hex(Crypt.hashSha512(data).substr(0, 64));
};
/**
* Sign a data string with a secret key
*
@@ -345,171 +358,5 @@ Crypt.base64UrlToBase64 = function(encodedData) {
return encodedData;
};
/**
* Create a string from request parameters that
* will be used to sign a request
*
* @param {Object} config - request params
* @param {Object} parsed - parsed url
* @param {Object} date
* @param {Object} mechanism - type of signing
*/
Crypt.getStringToSign = function(config, parsed, date, mechanism) {
// XXX This method doesn't handle signing GET requests correctly. The data
// field will be merged into the search string, not the request body.
// Sort the properties of the JSON object into canonical form
var canonicalData = JSON.stringify(copyObjectWithSortedKeys(config.data));
// Canonical request using Amazon's v4 signature format
// See: http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
var canonicalRequest = [
config.method || 'GET',
parsed.pathname || '',
parsed.search || '',
// XXX Headers signing not supported
'',
'',
Crypt.hashSha512(canonicalData).toLowerCase()
].join('\n');
// String to sign inspired by Amazon's v4 signature format
// See: http://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
//
// We don't have a credential scope, so we skip it.
//
// But that modifies the format, so the format ID is RIPPLE1, instead of AWS4.
return [
mechanism,
date,
Crypt.hashSha512(canonicalRequest).toLowerCase()
].join('\n');
};
/**
* HMAC signed request
*
* @param {Object} config
* @param {Object} auth_secret
* @param {Object} blob_id
*/
Crypt.signRequestHmac = function(config, auth_secret, blob_id) {
config = extend(true, {}, config);
// Parse URL
var parsed = parser.parse(config.url);
var date = dateAsIso8601();
var signatureType = 'RIPPLE1-HMAC-SHA512';
var stringToSign = Crypt.getStringToSign(config, parsed, date, signatureType);
var signature = Crypt.signString(auth_secret, stringToSign);
var query = querystring.stringify({
signature: Crypt.base64ToBase64Url(signature),
signature_date: date,
signature_blob_id: blob_id,
signature_type: signatureType
});
config.url += (parsed.search ? '&' : '?') + query;
return config;
};
/**
* Asymmetric signed request
*
* @param {Object} config
* @param {Object} secretKey
* @param {Object} account
* @param {Object} blob_id
*/
Crypt.signRequestAsymmetric = function(config, secretKey, account, blob_id) {
config = extend(true, {}, config);
// Parse URL
var parsed = parser.parse(config.url);
var date = dateAsIso8601();
var signatureType = 'RIPPLE1-ECDSA-SHA512';
var stringToSign = Crypt.getStringToSign(config, parsed, date, signatureType);
var signature = message.signMessage(stringToSign, secretKey);
var query = querystring.stringify({
signature: Crypt.base64ToBase64Url(signature),
signature_date: date,
signature_blob_id: blob_id,
signature_account: account,
signature_type: signatureType
})
config.url += (parsed.search ? '&' : '?') + query;
return config;
};
//prepare for signing
function copyObjectWithSortedKeys(object) {
if (isPlainObject(object)) {
var newObj = {};
var keysSorted = Object.keys(object).sort();
var key;
for (var i in keysSorted) {
key = keysSorted[i];
if (Object.prototype.hasOwnProperty.call(object, key)) {
newObj[key] = copyObjectWithSortedKeys(object[key]);
}
}
return newObj;
} else if (Array.isArray(object)) {
return object.map(copyObjectWithSortedKeys);
} else {
return object;
}
};
//from npm extend
function isPlainObject(obj) {
var hasOwn = Object.prototype.hasOwnProperty;
var toString = Object.prototype.toString;
if (!obj || toString.call(obj) !== '[object Object]' || obj.nodeType || obj.setInterval) {
return false;
}
var has_own_constructor = hasOwn.call(obj, 'constructor');
var has_is_property_of_method = hasOwn.call(obj.constructor.prototype, 'isPrototypeOf');
// Not own constructor property must be Object
if (obj.constructor && !has_own_constructor && !has_is_property_of_method) {
return false;
}
// Own properties are enumerated firstly, so to speed up,
// if last one is own, then all properties are own.
var key;
for ( key in obj ) {}
return key === void(0) || hasOwn.call( obj, key );
}
var dateAsIso8601 = (function() {
function pad(n) {
return (n < 0 || n > 9 ? '' : '0') + n;
};
return function dateAsIso8601() {
var date = new Date();
return date.getUTCFullYear() + "-" +
pad(date.getUTCMonth() + 1) + "-" +
pad(date.getUTCDate()) + "T" +
pad(date.getUTCHours()) + ":" +
pad(date.getUTCMinutes()) + ":" +
pad(date.getUTCSeconds()) + ".000Z";
};
})();
exports.Crypt = Crypt;

View File

@@ -11,7 +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.Message = require('./message').Message;
exports.VaultClient = require('./vaultclient').VaultClient;
exports.binformat = require('./binformat');
exports.utils = require('./utils');

View File

@@ -5,6 +5,11 @@ var SHAMap = require('./shamap').SHAMap;
var SHAMapTreeNode = require('./shamap').SHAMapTreeNode;
var SerializedObject = require('./serializedobject').SerializedObject;
var stypes = require('./serializedtypes');
var UInt160 = require('./uint160').UInt160;
var Currency = require('./currency').Currency;
var stypes = require('./serializedtypes');
var sjcl = require('./utils').sjcl;
var Crypt = require('./crypt').Crypt;
function Ledger()
{
@@ -17,6 +22,91 @@ Ledger.from_json = function (v) {
return ledger;
};
Ledger.space = require('./ledgerspaces');
/**
* Generate the key for an AccountRoot entry.
*
* @param {String|UInt160} account Ripple Account
* @return {UInt256}
*/
Ledger.calcAccountRootEntryHash =
Ledger.prototype.calcAccountRootEntryHash = function (account) {
account = UInt160.from_json(account);
var index = new SerializedObject();
index.append([0, Ledger.space.account.charCodeAt(0)]);
index.append(account.to_bytes());
return index.hash();
};
/**
* Generate the key for an Offer entry.
*
* @param {String|UInt160} account Ripple Account
* @param {Number} sequence Sequence number of the OfferCreate transaction
* that instantiated this offer.
* @return {UInt256}
*/
Ledger.calcOfferEntryHash =
Ledger.prototype.calcOfferEntryHash = function (account, sequence) {
account = UInt160.from_json(account);
sequence = parseInt(sequence);
var index = new SerializedObject();
index.append([0, Ledger.space.offer.charCodeAt(0)]);
index.append(account.to_bytes());
stypes.Int32.serialize(index, sequence);
return index.hash();
};
/**
* Generate the key for a RippleState entry.
*
* The ordering of the two account parameters does not matter.
*
* @param {String|UInt160} account1 First Ripple Account
* @param {String|UInt160} account2 Second Ripple Account
* @param {String|Currency} currency The currency code
* @return {UInt256}
*/
Ledger.calcRippleStateEntryHash =
Ledger.prototype.calcRippleStateEntryHash = function (account1, account2, currency) {
currency = Currency.from_json(currency);
account1 = UInt160.from_json(account1);
account2 = UInt160.from_json(account2);
if (!account1.is_valid()) {
throw new Error("Invalid first account");
}
if (!account2.is_valid()) {
throw new Error("Invalid second account");
}
if (!currency.is_valid()) {
throw new Error("Invalid currency");
}
// The lower ID has to come first
if (account1.to_bn().greaterEquals(account2.to_bn())) {
var tmp = account2;
account2 = account1;
account1 = tmp;
}
var index = new SerializedObject();
index.append([0, Ledger.space.rippleState.charCodeAt(0)]);
index.append(account1.to_bytes());
index.append(account2.to_bytes());
index.append(currency.to_bytes());
return index.hash();
};
Ledger.prototype.parse_json = function (v) {
this.ledger_json = v;
};

View File

@@ -0,0 +1,22 @@
/**
* Ripple ledger namespace prefixes.
*
* The Ripple ledger is a key-value store. In order to avoid name collisions,
* names are partitioned into namespaces.
*
* Each namespace is just a single character prefix.
*/
module.exports = {
account : 'a',
dirNode : 'd',
generatorMap : 'g',
nickname : 'n',
rippleState : 'r',
offer : 'o', // Entry for an offer.
ownerDir : 'O', // Directory of things owned by an account.
bookDir : 'B', // Directory of order books.
contract : 'c',
skipList : 's',
amendment : 'f',
feeSettings : 'e'
};

View File

@@ -200,4 +200,4 @@ Message.verifyHashSignature = function(data, remote, callback) {
};
module.exports = Message;
exports.Message = Message;

View File

@@ -3,6 +3,7 @@ var extend = require('extend');
var binformat = require('./binformat');
var stypes = require('./serializedtypes');
var UInt256 = require('./uint256').UInt256;
var Crypt = require('./crypt').Crypt;
var utils = require('./utils');
var sjcl = utils.sjcl;
@@ -247,20 +248,23 @@ SerializedObject.prototype.serialize = function(typedef, obj) {
SerializedObject.prototype.hash = function(prefix) {
var sign_buffer = new SerializedObject();
stypes.Int32.serialize(sign_buffer, prefix);
// Add hashing prefix
if ("undefined" !== typeof prefix) {
stypes.Int32.serialize(sign_buffer, prefix);
}
// Copy buffer to temporary buffer
sign_buffer.append(this.buffer);
return sign_buffer.hash_sha512_half();
// XXX We need a proper Buffer class then Crypt could accept that
var bits = sjcl.codec.bytes.toBits(sign_buffer.buffer);
return Crypt.hashSha512Half(bits);
};
// DEPRECATED
SerializedObject.prototype.signing_hash = SerializedObject.prototype.hash;
SerializedObject.prototype.hash_sha512_half = function() {
var bits = sjcl.codec.bytes.toBits(this.buffer);
var hash = sjcl.bitArray.bitSlice(sjcl.hash.sha512.hash(bits), 0, 256);
return UInt256.from_hex(sjcl.codec.hex.fromBits(hash));
};
SerializedObject.prototype.serialize_field = function(spec, obj) {
var name = spec[0];
var presence = spec[1];

View File

@@ -0,0 +1,174 @@
var Crypt = require('./crypt').Crypt;
var Message = require('./message').Message;
var parser = require("url");
var querystring = require('querystring');
var extend = require("extend");
var SignedRequest = function (config) {
// XXX Constructor should be generalized and constructing from an Angular.js
// $http config should be a SignedRequest.from... utility method.
this.config = extend(true, {}, config);
};
/**
* Create a string from request parameters that
* will be used to sign a request
* @param {Object} parsed - parsed url
* @param {Object} date
* @param {Object} mechanism - type of signing
*/
SignedRequest.prototype.getStringToSign = function (parsed, date, mechanism) {
// XXX This method doesn't handle signing GET requests correctly. The data
// field will be merged into the search string, not the request body.
// Sort the properties of the JSON object into canonical form
var canonicalData = JSON.stringify(copyObjectWithSortedKeys(this.config.data));
// Canonical request using Amazon's v4 signature format
// See: http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
var canonicalRequest = [
this.config.method || 'GET',
parsed.pathname || '',
parsed.search || '',
// XXX Headers signing not supported
'',
'',
Crypt.hashSha512(canonicalData).toLowerCase()
].join('\n');
// String to sign inspired by Amazon's v4 signature format
// See: http://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
//
// We don't have a credential scope, so we skip it.
//
// But that modifies the format, so the format ID is RIPPLE1, instead of AWS4.
return [
mechanism,
date,
Crypt.hashSha512(canonicalRequest).toLowerCase()
].join('\n');
}
//prepare for signing
function copyObjectWithSortedKeys(object) {
if (isPlainObject(object)) {
var newObj = {};
var keysSorted = Object.keys(object).sort();
var key;
for (var i in keysSorted) {
key = keysSorted[i];
if (Object.prototype.hasOwnProperty.call(object, key)) {
newObj[key] = copyObjectWithSortedKeys(object[key]);
}
}
return newObj;
} else if (Array.isArray(object)) {
return object.map(copyObjectWithSortedKeys);
} else {
return object;
}
}
//from npm extend
function isPlainObject(obj) {
var hasOwn = Object.prototype.hasOwnProperty;
var toString = Object.prototype.toString;
if (!obj || toString.call(obj) !== '[object Object]' || obj.nodeType || obj.setInterval)
return false;
var has_own_constructor = hasOwn.call(obj, 'constructor');
var has_is_property_of_method = hasOwn.call(obj.constructor.prototype, 'isPrototypeOf');
// Not own constructor property must be Object
if (obj.constructor && !has_own_constructor && !has_is_property_of_method)
return false;
// Own properties are enumerated firstly, so to speed up,
// if last one is own, then all properties are own.
var key;
for ( key in obj ) {}
return key === undefined || hasOwn.call( obj, key );
};
/**
* HMAC signed request
* @param {Object} config
* @param {Object} auth_secret
* @param {Object} blob_id
*/
SignedRequest.prototype.signHmac = function (auth_secret, blob_id) {
var config = extend(true, {}, this.config);
// Parse URL
var parsed = parser.parse(config.url);
var date = dateAsIso8601();
var signatureType = 'RIPPLE1-HMAC-SHA512';
var stringToSign = this.getStringToSign(parsed, date, signatureType);
var signature = Crypt.signString(auth_secret, stringToSign);
var query = querystring.stringify({
signature: Crypt.base64ToBase64Url(signature),
signature_date: date,
signature_blob_id: blob_id,
signature_type: signatureType
});
config.url += (parsed.search ? '&' : '?') + query;
return config;
};
/**
* Asymmetric signed request
* @param {Object} config
* @param {Object} secretKey
* @param {Object} account
* @param {Object} blob_id
*/
SignedRequest.prototype.signAsymmetric = function (secretKey, account, blob_id) {
var config = extend(true, {}, this.config);
// Parse URL
var parsed = parser.parse(config.url);
var date = dateAsIso8601();
var signatureType = 'RIPPLE1-ECDSA-SHA512';
var stringToSign = this.getStringToSign(parsed, date, signatureType);
var signature = Message.signMessage(stringToSign, secretKey);
var query = querystring.stringify({
signature: Crypt.base64ToBase64Url(signature),
signature_date: date,
signature_blob_id: blob_id,
signature_account: account,
signature_type: signatureType
});
config.url += (parsed.search ? '&' : '?') + query;
return config;
};
var dateAsIso8601 = (function () {
function pad(n) {
return (n < 0 || n > 9 ? "" : "0") + n;
}
return function dateAsIso8601() {
var date = new Date();
return date.getUTCFullYear() + "-"
+ pad(date.getUTCMonth() + 1) + "-"
+ pad(date.getUTCDate()) + "T"
+ pad(date.getUTCHours()) + ":"
+ pad(date.getUTCMinutes()) + ":"
+ pad(date.getUTCSeconds()) + ".000Z";
};
})();
// XXX Add methods for verifying requests
// SignedRequest.prototype.verifySignatureHmac
// SignedRequest.prototype.verifySignatureAsymetric
exports.SignedRequest = SignedRequest;

View File

@@ -34,6 +34,55 @@ describe('Ledger', function() {
create_ledger_test(38129);
// Because, why not.
create_ledger_test(40000);
describe('#calcAccountRootEntryHash', function () {
it('will calculate the AccountRoot entry hash for rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh', function () {
var account = 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh';
var expectedEntryHash = '2B6AC232AA4C4BE41BF49D2459FA4A0347E1B543A4C92FCEE0821C0201E2E9A8';
var actualEntryHash = Ledger.calcAccountRootEntryHash(account);
assert.equal(actualEntryHash.to_hex(), expectedEntryHash);
});
});
describe('#calcRippleStateEntryHash', function () {
it('will calculate the RippleState entry hash for rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh and rB5TihdPbKgMrkFqrqUC3yLdE8hhv4BdeY in USD', function () {
var account1 = 'rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh';
var account2 = 'rB5TihdPbKgMrkFqrqUC3yLdE8hhv4BdeY';
var currency = 'USD';
var expectedEntryHash = 'C683B5BB928F025F1E860D9D69D6C554C2202DE0D45877ADB3077DA4CB9E125C';
var actualEntryHash1 = Ledger.calcRippleStateEntryHash(account1, account2, currency);
var actualEntryHash2 = Ledger.calcRippleStateEntryHash(account2, account1, currency);
assert.equal(actualEntryHash1.to_hex(), expectedEntryHash);
assert.equal(actualEntryHash2.to_hex(), expectedEntryHash);
});
it('will calculate the RippleState entry hash for r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV and rUAMuQTfVhbfqUDuro7zzy4jj4Wq57MPTj in UAM', function () {
var account1 = 'r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV';
var account2 = 'rUAMuQTfVhbfqUDuro7zzy4jj4Wq57MPTj';
var currency = 'UAM';
var expectedEntryHash = 'AE9ADDC584358E5847ADFC971834E471436FC3E9DE6EA1773DF49F419DC0F65E';
var actualEntryHash1 = Ledger.calcRippleStateEntryHash(account1, account2, currency);
var actualEntryHash2 = Ledger.calcRippleStateEntryHash(account2, account1, currency);
assert.equal(actualEntryHash1.to_hex(), expectedEntryHash);
assert.equal(actualEntryHash2.to_hex(), expectedEntryHash);
});
});
describe('#calcOfferEntryHash', function () {
it('will calculate the Offer entry hash for r32UufnaCGL82HubijgJGDmdE5hac7ZvLw, sequence 137', function () {
var account = 'r32UufnaCGL82HubijgJGDmdE5hac7ZvLw';
var sequence = 137
var expectedEntryHash = '03F0AED09DEEE74CEF85CD57A0429D6113507CF759C597BABB4ADB752F734CE3';
var actualEntryHash = Ledger.calcOfferEntryHash(account, sequence);
assert.equal(actualEntryHash.to_hex(), expectedEntryHash);
});
});
});
// vim:sw=2:sts=2:ts=8:et

View File

@@ -1,6 +1,6 @@
var assert = require('assert');
var sjcl = require('../build/sjcl');
var Message = require('../src/js/ripple/message');
var Message = require('../src/js/ripple/message').Message;
var Seed = require('../src/js/ripple/seed').Seed;
var Remote = require('../src/js/ripple/remote').Remote;