Add SHAMaps, metadata serialization and transaction hash calculation.

This commit is contained in:
Stefan Thomas
2013-11-07 22:36:06 -08:00
parent 74ef8f8400
commit 5def7ba917
9 changed files with 544 additions and 178 deletions

View File

@@ -0,0 +1,14 @@
var fs = require('fs');
var Ledger = require('../src/js/ripple/ledger').Ledger;
if (process.argc < 1) {
console.error("Usage: scripts/verify_ledger_json path/to/ledger.json");
process.exit(1);
}
var json = fs.readFileSync(process.argv[2], 'utf-8');
var ledger = Ledger.from_json(JSON.parse(json));
console.log("Calculated transaction hash: "+ledger.calc_tx_hash().to_hex())
console.log("Transaction hash in header: "+ledger.ledger_json.transaction_hash);

View File

@@ -1,90 +1,114 @@
var ST = require("./serializedtypes");
var REQUIRED = exports.REQUIRED = 0,
OPTIONAL = exports.OPTIONAL = 1,
DEFAULT = exports.DEFAULT = 2;
ST.Int16.id = 1;
ST.Int32.id = 2;
ST.Int64.id = 3;
ST.Hash128.id = 4;
ST.Hash256.id = 5;
ST.Amount.id = 6;
ST.VariableLength.id = 7;
ST.Account.id = 8;
ST.Object.id = 14;
ST.Array.id = 15;
ST.Int8.id = 16;
ST.Hash160.id = 17;
ST.PathSet.id = 18;
ST.Vector256.id = 19;
var base = [
[ 'TransactionType' , REQUIRED, 2, ST.Int16 ],
[ 'Flags' , OPTIONAL, 2, ST.Int32 ],
[ 'SourceTag' , OPTIONAL, 3, ST.Int32 ],
[ 'Account' , REQUIRED, 1, ST.Account ],
[ 'Sequence' , REQUIRED, 4, ST.Int32 ],
[ 'Fee' , REQUIRED, 8, ST.Amount ],
[ 'OperationLimit' , OPTIONAL, 29, ST.Int32 ],
[ 'SigningPubKey' , REQUIRED, 3, ST.VariableLength ],
[ 'TxnSignature' , OPTIONAL, 4, ST.VariableLength ]
[ 'TransactionType' , REQUIRED, 2, "Int16" ],
[ 'Flags' , OPTIONAL, 2, "Int32" ],
[ 'SourceTag' , OPTIONAL, 3, "Int32" ],
[ 'Account' , REQUIRED, 1, "Account" ],
[ 'Sequence' , REQUIRED, 4, "Int32" ],
[ 'Fee' , REQUIRED, 8, "Amount" ],
[ 'OperationLimit' , OPTIONAL, 29, "Int32" ],
[ 'SigningPubKey' , REQUIRED, 3, "VariableLength" ],
[ 'TxnSignature' , OPTIONAL, 4, "VariableLength" ]
];
exports.tx = {
AccountSet: [3].concat(base, [
[ 'EmailHash' , OPTIONAL, 1, ST.Hash128 ],
[ 'WalletLocator' , OPTIONAL, 7, ST.Hash256 ],
[ 'WalletSize' , OPTIONAL, 12, ST.Int32 ],
[ 'MessageKey' , OPTIONAL, 2, ST.VariableLength ],
[ 'Domain' , OPTIONAL, 7, ST.VariableLength ],
[ 'TransferRate' , OPTIONAL, 11, ST.Int32 ]
[ 'EmailHash' , OPTIONAL, 1, "Hash128" ],
[ 'WalletLocator' , OPTIONAL, 7, "Hash256" ],
[ 'WalletSize' , OPTIONAL, 12, "Int32" ],
[ 'MessageKey' , OPTIONAL, 2, "VariableLength" ],
[ 'Domain' , OPTIONAL, 7, "VariableLength" ],
[ 'TransferRate' , OPTIONAL, 11, "Int32" ]
]),
TrustSet: [20].concat(base, [
[ 'LimitAmount' , OPTIONAL, 3, ST.Amount ],
[ 'QualityIn' , OPTIONAL, 20, ST.Int32 ],
[ 'QualityOut' , OPTIONAL, 21, ST.Int32 ]
[ 'LimitAmount' , OPTIONAL, 3, "Amount" ],
[ 'QualityIn' , OPTIONAL, 20, "Int32" ],
[ 'QualityOut' , OPTIONAL, 21, "Int32" ]
]),
OfferCreate: [7].concat(base, [
[ 'TakerPays' , REQUIRED, 4, ST.Amount ],
[ 'TakerGets' , REQUIRED, 5, ST.Amount ],
[ 'Expiration' , OPTIONAL, 10, ST.Int32 ]
[ 'TakerPays' , REQUIRED, 4, "Amount" ],
[ 'TakerGets' , REQUIRED, 5, "Amount" ],
[ 'Expiration' , OPTIONAL, 10, "Int32" ]
]),
OfferCancel: [8].concat(base, [
[ 'OfferSequence' , REQUIRED, 25, ST.Int32 ]
[ 'OfferSequence' , REQUIRED, 25, "Int32" ]
]),
SetRegularKey: [5].concat(base, [
[ 'RegularKey' , REQUIRED, 8, ST.Account ]
[ 'RegularKey' , REQUIRED, 8, "Account" ]
]),
Payment: [0].concat(base, [
[ 'Destination' , REQUIRED, 3, ST.Account ],
[ 'Amount' , REQUIRED, 1, ST.Amount ],
[ 'SendMax' , OPTIONAL, 9, ST.Amount ],
[ 'Paths' , DEFAULT , 1, ST.PathSet ],
[ 'InvoiceID' , OPTIONAL, 17, ST.Hash256 ],
[ 'DestinationTag' , OPTIONAL, 14, ST.Int32 ]
[ 'Destination' , REQUIRED, 3, "Account" ],
[ 'Amount' , REQUIRED, 1, "Amount" ],
[ 'SendMax' , OPTIONAL, 9, "Amount" ],
[ 'Paths' , DEFAULT , 1, "PathSet" ],
[ 'InvoiceID' , OPTIONAL, 17, "Hash256" ],
[ 'DestinationTag' , OPTIONAL, 14, "Int32" ]
]),
Contract: [9].concat(base, [
[ 'Expiration' , REQUIRED, 10, ST.Int32 ],
[ 'BondAmount' , REQUIRED, 23, ST.Int32 ],
[ 'StampEscrow' , REQUIRED, 22, ST.Int32 ],
[ 'RippleEscrow' , REQUIRED, 17, ST.Amount ],
[ 'CreateCode' , OPTIONAL, 11, ST.VariableLength ],
[ 'FundCode' , OPTIONAL, 8, ST.VariableLength ],
[ 'RemoveCode' , OPTIONAL, 9, ST.VariableLength ],
[ 'ExpireCode' , OPTIONAL, 10, ST.VariableLength ]
[ 'Expiration' , REQUIRED, 10, "Int32" ],
[ 'BondAmount' , REQUIRED, 23, "Int32" ],
[ 'StampEscrow' , REQUIRED, 22, "Int32" ],
[ 'RippleEscrow' , REQUIRED, 17, "Amount" ],
[ 'CreateCode' , OPTIONAL, 11, "VariableLength" ],
[ 'FundCode' , OPTIONAL, 8, "VariableLength" ],
[ 'RemoveCode' , OPTIONAL, 9, "VariableLength" ],
[ 'ExpireCode' , OPTIONAL, 10, "VariableLength" ]
]),
RemoveContract: [10].concat(base, [
[ 'Target' , REQUIRED, 7, ST.Account ]
[ 'Target' , REQUIRED, 7, "Account" ]
]),
EnableFeature: [100].concat(base, [
[ 'Feature' , REQUIRED, 19, ST.Hash256 ]
[ 'Feature' , REQUIRED, 19, "Hash256" ]
]),
SetFee: [101].concat(base, [
[ 'Features' , REQUIRED, 9, ST.Array ],
[ 'BaseFee' , REQUIRED, 5, ST.Int64 ],
[ 'ReferenceFeeUnits' , REQUIRED, 30, ST.Int32 ],
[ 'ReserveBase' , REQUIRED, 31, ST.Int32 ],
[ 'ReserveIncrement' , REQUIRED, 32, ST.Int32 ]
[ 'Features' , REQUIRED, 9, "Array" ],
[ 'BaseFee' , REQUIRED, 5, "Int64" ],
[ 'ReferenceFeeUnits' , REQUIRED, 30, "Int32" ],
[ 'ReserveBase' , REQUIRED, 31, "Int32" ],
[ 'ReserveIncrement' , REQUIRED, 32, "Int32" ]
])
};
exports.ledger = {
AccountRoot: [97],
Contract: [99],
DirectoryNode: [100],
Features: [102],
GeneratorMap: [103],
LedgerHashes: [104],
Nickname: [110],
Offer: [111],
RippleState: [114],
FeeSettings: [115]
};
exports.metadata = [
[ 'TransactionIndex' , REQUIRED, 28, "Int32" ],
[ 'TransactionResult' , REQUIRED, 3, "Int8" ],
[ 'AffectedNodes' , REQUIRED, 8, "Array" ]
];
exports.ter = {
tesSUCCESS: 0,
tecCLAIM: 100,
tecPATH_PARTIAL: 101,
tecUNFUNDED_ADD: 102,
tecUNFUNDED_OFFER: 103,
tecUNFUNDED_PAYMENT: 104,
tecFAILED_PROCESSING: 105,
tecDIR_FULL: 121,
tecINSUF_RESERVE_LINE: 122,
tecINSUF_RESERVE_OFFER: 123,
tecNO_DST: 124,
tecNO_DST_INSUF_XRP: 125,
tecNO_LINE_INSUF_RESERVE: 126,
tecNO_LINE_REDUNDANT: 127,
tecPATH_DRY: 128,
tecUNFUNDED: 129,
tecMASTER_DISABLED: 130,
tecNO_REGULAR_KEY: 131,
tecOWNERS: 132
};

View File

@@ -0,0 +1,23 @@
/**
* Prefix for hashing functions.
*
* These prefixes are inserted before the source material used to
* generate various hashes. This is done to put each hash in its own
* "space." This way, two different types of objects with the
* same binary data will produce different hashes.
*
* Each prefix is a 4-byte value with the last byte set to zero
* and the first three bytes formed from the ASCII equivalent of
* some arbitrary string. For example "TXN".
*/
// transaction plus signature to give transaction ID
exports.HASH_TX_ID = 0x54584E00; // 'TXN'
// transaction plus metadata
exports.HASH_TX_NODE = 0x534E4400; // 'TND'
// inner node in tree
exports.HASH_INNER_NODE = 0x4D494E00; // 'MIN'
// inner transaction to sign
exports.HASH_TX_SIGN = 0x53545800; // 'STX'
// inner transaction to sign (TESTNET)
exports.HASH_TX_SIGN_TESTNET = 0x73747800; // 'stx'

40
src/js/ripple/ledger.js Normal file
View File

@@ -0,0 +1,40 @@
// Ledger
var Transaction = require('./transaction').Transaction;
var SHAMap = require('./shamap').SHAMap;
var SHAMapTreeNode = require('./shamap').SHAMapTreeNode;
var SerializedObject = require('./serializedobject').SerializedObject;
var stypes = require('./serializedtypes');
function Ledger()
{
this.ledger_json = {};
}
Ledger.from_json = function (v) {
var ledger = new Ledger();
ledger.parse_json(v);
return ledger;
};
Ledger.prototype.parse_json = function (v) {
this.ledger_json = v;
};
Ledger.prototype.calc_tx_hash = function () {
var tx_map = new SHAMap();
this.ledger_json.transactions.forEach(function (tx_json) {
var tx = Transaction.from_json(tx_json);
var meta = SerializedObject.from_json(tx_json.metaData);
var data = new SerializedObject();
stypes.VariableLength.serialize(data, tx.serialize().to_hex());
stypes.VariableLength.serialize(data, meta.to_hex());
tx_map.add_item(tx.hash(), data, SHAMapTreeNode.TYPE_TRANSACTION_MD);
});
return tx_map.hash();
};
exports.Ledger = Ledger;

View File

@@ -1,62 +1,29 @@
var binformat = require('./binformat');
var sjcl = require('./utils').sjcl;
var extend = require('extend');
var stypes = require('./serializedtypes');
var UInt256 = require('./uint256').UInt256;
var assert = require('assert');
var binformat = require('./binformat');
var extend = require('extend');
var stypes = require('./serializedtypes');
var UInt256 = require('./uint256').UInt256;
var assert = require('assert');
var TRANSACTION_TYPES = {
0: 'Payment',
3: 'AccountSet',
5: 'SetRegularKey',
7: 'OfferCreate',
8: 'OfferCancel',
9: 'Contract',
10: 'RemoveContract',
20: 'TrustSet',
100: 'EnableFeature',
101: 'SetFee'
};
var utils = require('./utils');
var sjcl = utils.sjcl;
var BigInteger = utils.jsbn.BigInteger;
var LEDGER_ENTRY_TYPES = {
97: 'AccountRoot',
99: 'Contract',
100: 'DirectoryNode',
102: 'Features',
103: 'GeneratorMap',
104: 'LedgerHashes',
110: 'Nickname',
111: 'Offer',
114: 'RippleState',
115: 'FeeSettings'
};
var TRANSACTION_RESULTS = {
0 : 'tesSUCCESS',
100: 'tecCLAIM',
101: 'tecPATH_PARTIAL',
102: 'tecUNFUNDED_ADD',
103: 'tecUNFUNDED_OFFER',
104: 'tecUNFUNDED_PAYMENT',
105: 'tecFAILED_PROCESSING',
121: 'tecDIR_FULL',
122: 'tecINSUF_RESERVE_LINE',
123: 'tecINSUF_RESERVE_OFFER',
124: 'tecNO_DST',
125: 'tecNO_DST_INSUF_XRP',
126: 'tecNO_LINE_INSUF_RESERVE',
127: 'tecNO_LINE_REDUNDANT',
128: 'tecPATH_DRY',
129: 'tecUNFUNDED', // Deprecated, old ambiguous unfunded.
130: 'tecMASTER_DISABLED',
131: 'tecNO_REGULAR_KEY',
132: 'tecOWNERS'
};
var TX_ID_MAP = { };
var TRANSACTION_TYPES = { };
Object.keys(binformat.tx).forEach(function(key) {
TX_ID_MAP[key[0]] = key;
TRANSACTION_TYPES[binformat.tx[key][0]] = key;
});
var LEDGER_ENTRY_TYPES = {};
Object.keys(binformat.ledger).forEach(function(key) {
LEDGER_ENTRY_TYPES[binformat.ledger[key][0]] = key;
});
var TRANSACTION_RESULTS = {};
Object.keys(binformat.ter).forEach(function(key) {
TRANSACTION_RESULTS[binformat.ter[key]] = key;
});
function SerializedObject(buf) {
@@ -78,39 +45,42 @@ SerializedObject.from_json = function (obj) {
var so = new SerializedObject;
var typedef;
switch (typeof obj.TransactionType) {
case 'number':
obj.TransactionType = SerializedObject.lookup_type_tx(obj.TransactionType);
if ("number" === typeof obj.TransactionType) {
obj.TransactionType = SerializedObject.lookup_type_tx(obj.TransactionType);
if (!obj.TransactionType) {
throw new Error('Transaction type ID is invalid.');
}
break;
case 'string':
typedef = binformat.tx[obj.TransactionType];
if (!Array.isArray(typedef)) {
throw new Error('Transaction type is invalid');
}
typedef = typedef.slice();
obj.TransactionType = typedef.shift();
break;
default:
if (typeof obj.LedgerEntryType !== 'undefined') {
// XXX: TODO
throw new Error('Ledger entry binary format not yet implemented.');
} else {
throw new Error('Object to be serialized must contain either ' + 'TransactionType or LedgerEntryType.');
if (!obj.TransactionType) {
throw new Error('Transaction type ID is invalid.');
}
}
if ("string" === typeof obj.TransactionType) {
typedef = binformat.tx[obj.TransactionType];
if (!Array.isArray(typedef)) {
throw new Error('Transaction type is invalid');
}
typedef = typedef.slice();
obj.TransactionType = typedef.shift();
} else if ("undefined" !== typeof obj.LedgerEntryType) {
// XXX: TODO
throw new Error('Ledger entry binary format not yet implemented.');
} else if ("object" === typeof obj.AffectedNodes) {
typedef = binformat.metadata;
} else {
throw new Error('Object to be serialized must contain either' +
' TransactionType, LedgerEntryType or AffectedNodes.');
}
so.serialize(typedef, obj);
return so;
};
SerializedObject.prototype.append = function (bytes) {
if (bytes instanceof SerializedObject) {
bytes = bytes.buffer;
}
this.buffer = this.buffer.concat(bytes);
this.pointer += bytes.length;
};
@@ -174,13 +144,13 @@ SerializedObject.jsonify_structure = function(structure, field_name) {
case 'number':
switch (field_name) {
case 'LedgerEntryType':
output = LEDGER_ENTRY_TYPES[structure] || thing;
output = LEDGER_ENTRY_TYPES[structure];
break;
case 'TransactionResult':
output = TRANSACTION_RESULTS[structure] || thing;
output = TRANSACTION_RESULTS[structure];
break;
case 'TransactionType':
output = TRANSACTION_TYPES[structure] || thing;
output = TRANSACTION_TYPES[structure];
break;
default:
output = structure;
@@ -190,6 +160,8 @@ SerializedObject.jsonify_structure = function(structure, field_name) {
if (!structure) break; //null
if (typeof structure.to_json === 'function') {
output = structure.to_json();
} else if (structure instanceof BigInteger) {
output = structure.toString(16).toUpperCase();
} else {
output = new structure.constructor; //new Array or Object
var keys = Object.keys(structure);
@@ -207,13 +179,19 @@ SerializedObject.jsonify_structure = function(structure, field_name) {
};
SerializedObject.prototype.serialize = function (typedef, obj) {
// Serialize object without end marker
stypes.Object.serialize(this, obj, true);
// ST: Old serialization
/*
// Ensure canonical order
var typedef = SerializedObject.sort_typedef(typedef);
typedef = SerializedObject.sort_typedef(typedef);
// Serialize fields
for (var i=0, l=typedef.length; i<l; i++) {
this.serialize_field(typedef[i], obj);
}
*/
};
SerializedObject.prototype.hash = function (prefix) {
@@ -236,12 +214,15 @@ SerializedObject.prototype.serialize_field = function (spec, obj) {
var name = spec[0];
var presence = spec[1];
var field_id = spec[2];
var Type = spec[3];
var Type = stypes[spec[3]];
if (typeof obj[name] !== 'undefined') {
this.append(SerializedObject.get_field_header(Type.id, field_id));
// ST: Old serialization code
//this.append(SerializedObject.get_field_header(Type.id, field_id));
try {
Type.serialize(this, obj[name]);
// ST: Old serialization code
//Type.serialize(this, obj[name]);
stypes.serialize(this, name, obj[name]);
} catch (e) {
// Add field name to message and rethrow
e.message = 'Error serializing "' + name + '": ' + e.message;
@@ -275,15 +256,15 @@ SerializedObject.sort_typedef = function (typedef) {
function sort_field_compare(a, b) {
// Sort by type id first, then by field id
return a[3].id !== b[3].id ? a[3].id - b[3].id : a[2] - b[2];
return a[3] !== b[3] ? stypes[a[3]].id - stypes[b[3]].id : a[2] - b[2];
};
return typedef.sort(sort_field_compare);
};
SerializedObject.lookup_type_tx = function (id) {
assert(typeof id === 'string');
return TX_ID_MAP[id];
assert(typeof id === 'number');
return TRANSACTION_TYPES[id];
};
exports.SerializedObject = SerializedObject;

View File

@@ -6,25 +6,27 @@
* SerializedObject.parse() or SerializedObject.serialize().
*/
var assert = require('assert');
var extend = require('extend');
var utils = require('./utils');
var sjcl = utils.sjcl;
var assert = require('assert');
var extend = require('extend');
var binformat = require('./binformat');
var utils = require('./utils');
var sjcl = utils.sjcl;
var UInt128 = require('./uint128').UInt128;
var UInt160 = require('./uint160').UInt160;
var UInt256 = require('./uint256').UInt256;
var UInt128 = require('./uint128').UInt128;
var UInt160 = require('./uint160').UInt160;
var UInt256 = require('./uint256').UInt256;
var amount = require('./amount');
var Amount = amount.Amount;
var Currency = amount.Currency;
var amount = require('./amount');
var Amount = amount.Amount;
var Currency = amount.Currency;
// Shortcuts
var hex = sjcl.codec.hex;
var bytes = sjcl.codec.bytes;
var hex = sjcl.codec.hex;
var bytes = sjcl.codec.bytes;
var BigInteger = utils.jsbn.BigInteger;
var SerializedType = function (methods) {
extend(this, methods);
};
@@ -154,6 +156,8 @@ var STInt8 = exports.Int8 = new SerializedType({
}
});
STInt8.id = 16;
var STInt16 = exports.Int16 = new SerializedType({
serialize: function (so, val) {
append_byte_array(so, val, 2);
@@ -163,6 +167,8 @@ var STInt16 = exports.Int16 = new SerializedType({
}
});
STInt16.id = 1;
var STInt32 = exports.Int32 = new SerializedType({
serialize: function (so, val) {
append_byte_array(so, val, 4)
@@ -172,6 +178,8 @@ var STInt32 = exports.Int32 = new SerializedType({
}
});
STInt32.id = 2;
var STInt64 = exports.Int64 = new SerializedType({
serialize: function (so, val) {
var bigNumObject;
@@ -215,6 +223,8 @@ var STInt64 = exports.Int64 = new SerializedType({
}
});
STInt64.id = 3;
var STHash128 = exports.Hash128 = new SerializedType({
serialize: function (so, val) {
var hash = UInt128.from_json(val);
@@ -228,6 +238,8 @@ var STHash128 = exports.Hash128 = new SerializedType({
}
});
STHash128.id = 4;
var STHash256 = exports.Hash256 = new SerializedType({
serialize: function (so, val) {
var hash = UInt256.from_json(val);
@@ -241,6 +253,8 @@ var STHash256 = exports.Hash256 = new SerializedType({
}
});
STHash256.id = 5;
var STHash160 = exports.Hash160 = new SerializedType({
serialize: function (so, val) {
var hash = UInt160.from_json(val);
@@ -254,6 +268,8 @@ var STHash160 = exports.Hash160 = new SerializedType({
}
});
STHash160.id = 17;
// Internal
var STCurrency = new SerializedType({
serialize: function (so, val) {
@@ -385,6 +401,8 @@ var STAmount = exports.Amount = new SerializedType({
}
});
STAmount.id = 6;
var STVL = exports.VariableLength = new SerializedType({
serialize: function (so, val) {
if (typeof val === 'string') {
@@ -399,6 +417,8 @@ var STVL = exports.VariableLength = new SerializedType({
}
});
STVL.id = 7;
var STAccount = exports.Account = new SerializedType({
serialize: function (so, val) {
var account = UInt160.from_json(val);
@@ -425,6 +445,8 @@ var STAccount = exports.Account = new SerializedType({
}
});
STAccount.id = 8;
var STPathSet = exports.PathSet = new SerializedType({
typeBoundary: 0xff,
typeEnd: 0x00,
@@ -529,6 +551,8 @@ var STPathSet = exports.PathSet = new SerializedType({
}
});
STPathSet.id = 18;
var STVector256 = exports.Vector256 = new SerializedType({
serialize: function (so, val) { //Assume val is an array of STHash256 objects.
var length_as_varint = SerializedType.serialize_varint(so, val.length);
@@ -546,6 +570,8 @@ var STVector256 = exports.Vector256 = new SerializedType({
}
});
STVector256.id = 19;
exports.serialize = exports.serialize_whatever = serialize;
function serialize(so, field_name, value) {
@@ -555,16 +581,24 @@ function serialize(so, field_name, value) {
var field_coordinates = INVERSE_FIELDS_MAP[field_name];
var type_bits = field_coordinates[0];
var field_bits = field_coordinates[1];
var tag_byte = (type_bits < 16 ? type_bits << 4 : 0) | (field_bits < 16 ? field_bits : 0)
var tag_byte = (type_bits < 16 ? type_bits << 4 : 0) | (field_bits < 16 ? field_bits : 0);
STInt8.serialize(so, tag_byte)
if (field_name === "LedgerEntryType" && "string" === typeof value) {
value = binformat.ledger[value][0];
}
if (field_name === "TransactionResult" && "string" === typeof value) {
value = binformat.ter[value];
}
STInt8.serialize(so, tag_byte);
if (type_bits >= 16) {
STInt8.serialize(so, type_bits)
STInt8.serialize(so, type_bits);
}
if (field_bits >= 16) {
STInt8.serialize(so, field_bits)
STInt8.serialize(so, field_bits);
}
var serialized_object_type = TYPES_MAP[type_bits];
@@ -597,13 +631,39 @@ function parse(so) {
return [ field_name, type.parse(so) ]; //key, value
};
function sort_fields(keys) {
function sort_field_compare(a, b) {
var a_field_coordinates = INVERSE_FIELDS_MAP[a];
var a_type_bits = a_field_coordinates[0];
var a_field_bits = a_field_coordinates[1];
var b_field_coordinates = INVERSE_FIELDS_MAP[b];
var b_type_bits = b_field_coordinates[0];
var b_field_bits = b_field_coordinates[1];
// Sort by type id first, then by field id
return a_type_bits !== b_type_bits ? a_type_bits - b_type_bits : a_field_bits - b_field_bits;
};
return keys.sort(sort_field_compare);
}
var STObject = exports.Object = new SerializedType({
serialize: function (so, val) {
serialize: function (so, val, no_marker) {
var keys = Object.keys(val);
// Ignore lowercase field names - they're non-serializable fields by
// convention.
keys = keys.filter(function (key) {
return key[0] !== key[0].toLowerCase();
});
// Sort fields
keys = sort_fields(keys);
for (var i=0; i<keys.length; i++) {
serialize(so, keys[i], val[keys[i]]);
}
STInt8.serialize(so, 0xe1); //Object ending marker
if (!no_marker) STInt8.serialize(so, 0xe1); //Object ending marker
},
parse: function (so) {
@@ -617,6 +677,8 @@ var STObject = exports.Object = new SerializedType({
}
});
STObject.id = 14;
var STArray = exports.Array = new SerializedType({
serialize: function (so, val) {
for (var i=0, l=val.length; i<l; i++) {
@@ -649,6 +711,8 @@ var STArray = exports.Array = new SerializedType({
}
});
STArray.id = 15;
var TYPES_MAP = [
void(0),

174
src/js/ripple/shamap.js Normal file
View File

@@ -0,0 +1,174 @@
var util = require('util');
var sjcl = require('./utils').sjcl;
var stypes = require('./serializedtypes');
var hashprefixes = require('./hashprefixes');
var UInt256 = require('./uint256').UInt256;
var SerializedObject = require('./serializedobject').SerializedObject;
function SHAMap() {
this.root = new SHAMapTreeNodeInner();
};
SHAMap.prototype.add_item = function (tag, node, type) {
var node = new SHAMapTreeNodeLeaf(tag, node, type);
this.root.add_item(tag, node);
};
SHAMap.prototype.hash = function () {
return this.root.hash();
};
/**
* Abstract class representing a node in a SHAMap tree.
*
* Can be either SHAMapTreeNodeInner or SHAMapTreeNodeLeaf.
*/
function SHAMapTreeNode() {
};
SHAMapTreeNode.TYPE_INNER = 1;
SHAMapTreeNode.TYPE_TRANSACTION_NM = 2;
SHAMapTreeNode.TYPE_TRANSACTION_MD = 3;
SHAMapTreeNode.TYPE_ACCOUNT_STATE = 4;
SHAMapTreeNode.prototype.add_item = function (tag_segment, node) {
throw new Error("Called unimplemented virtual method SHAMapTreeNode#add_item.");
};
SHAMapTreeNode.prototype.hash = function () {
throw new Error("Called unimplemented virtual method SHAMapTreeNode#hash.");
};
/**
* Inner (non-leaf) node in a SHAMap tree.
*/
function SHAMapTreeNodeInner() {
SHAMapTreeNode.call(this);
this.leaves = {};
this.type = SHAMapTreeNode.INNER;
this.empty = true;
}
util.inherits(SHAMapTreeNodeInner, SHAMapTreeNode);
SHAMapTreeNodeInner.prototype.add_item = function (tag_segment, node) {
var current_node = this.get_node(tag_segment);
if (current_node) {
// A node already exists in this slot
if (current_node instanceof SHAMapTreeNodeInner) {
// There is an inner node, so we need to go deeper
current_node.add_item(tag_segment.slice(1), node);
} else if (current_node.get_segment() === tag_segment) {
// Collision
throw new Error("Tried to add a node to a SHAMap that was already in there.");
} else {
// Turn it into an inner node
var new_inner_node = new SHAMapTreeNodeInner();
// Move the existing leaf node down one level
current_node.set_segment(current_node.get_segment().slice(1));
new_inner_node.set_node(current_node.get_segment()[0], current_node);
// Add the new node next to it
node.set_segment(tag_segment.slice(1));
new_inner_node.set_node(tag_segment[1], node);
// And place the newly created inner node in the slot
this.set_node(tag_segment[0], new_inner_node);
}
} else {
// Neat, we have a nice open spot for the new node
node.set_segment(tag_segment);
this.set_node(tag_segment[0], node);
}
};
/**
* Overwrite the node that is currently in a given slot.
*/
SHAMapTreeNodeInner.prototype.set_node = function (slot, node) {
this.leaves[slot] = node;
this.empty = false;
};
SHAMapTreeNodeInner.prototype.get_node = function (slot) {
return this.leaves[slot];
};
SHAMapTreeNodeInner.prototype.hash = function () {
if (this.empty) {
return UInt256.from_hex(UInt256.HEX_ZERO);
}
var hash_buffer = new SerializedObject();
var buffer = [];
for (var i = 0; i < 16; i++) {
var leafHash = UInt256.from_hex(UInt256.HEX_ZERO);
var slot = i.toString(16).toUpperCase();
if ("object" === typeof this.leaves[slot]) {
leafHash = this.leaves[slot].hash();
}
hash_buffer.append(leafHash.to_bytes());
}
var hash = hash_buffer.hash(hashprefixes.HASH_INNER_NODE);
return UInt256.from_bits(hash);
};
/**
* Leaf node in a SHAMap tree.
*/
function SHAMapTreeNodeLeaf(tag, node, type) {
SHAMapTreeNode.call(this);
if ("string" === typeof tag) {
tag = UInt256.from_hex(tag);
} else if (tag instanceof UInt256) {
// Type is already the right one
} else {
throw new Error("Tag is unexpected type.");
}
this.tag = tag;
this.tag_segment = null;
this.type = type;
this.node = node;
}
util.inherits(SHAMapTreeNodeLeaf, SHAMapTreeNode);
SHAMapTreeNodeLeaf.prototype.get_segment = function (segment) {
return this.tag_segment;
};
SHAMapTreeNodeLeaf.prototype.set_segment = function (segment) {
this.tag_segment = segment;
};
SHAMapTreeNodeLeaf.prototype.hash = function () {
var buffer = new SerializedObject();
switch (this.type) {
case SHAMapTreeNode.TYPE_TRANSACTION_NM:
return this.tag;
case SHAMapTreeNode.TYPE_TRANSACTION_MD:
buffer.append(this.node);
buffer.append(this.tag.to_bytes());
return buffer.hash(hashprefixes.HASH_TX_NODE);
default:
throw new Error("Tried to hash a SHAMap node of unknown type.");
}
};
exports.SHAMap = SHAMap;
exports.SHAMapTreeNode = SHAMapTreeNode;
exports.SHAMapTreeNodeInner = SHAMapTreeNodeInner;
exports.SHAMapTreeNodeLeaf = SHAMapTreeNodeLeaf;

View File

@@ -54,6 +54,8 @@ var Seed = require('./seed').Seed;
var SerializedObject = require('./serializedobject').SerializedObject;
var RippleError = require('./rippleerror').RippleError;
var hashprefixes = require('./hashprefixes');
var config = require('./config');
// A class to implement transactions.
@@ -120,13 +122,6 @@ Transaction.flags = {
Transaction.formats = require('./binformat').tx;
// transaction plus signature to give transaction ID
Transaction.HASH_TXID = 0x54584E00; // 'TXN'
// inner transaction to sign
Transaction.HASH_SIGN = 0x53545800; // 'STX'
// inner transaction to sign (TESTNET)
Transaction.HASH_SIGN_TESTNET = 0x73747800; // 'stx'
Transaction.prototype.consts = {
telLOCAL_ERROR : -399,
temMALFORMED : -299,
@@ -212,17 +207,17 @@ Transaction.prototype.serialize = function () {
};
Transaction.prototype.signing_hash = function () {
return this.hash(config.testnet ? 'HASH_SIGN_TESTNET' : 'HASH_SIGN');
return this.hash(config.testnet ? 'HASH_TX_SIGN_TESTNET' : 'HASH_TX_SIGN');
};
Transaction.prototype.hash = function (prefix, as_uint256) {
if ("string" === typeof prefix) {
if ("undefined" === typeof Transaction[prefix]) {
if ("undefined" === typeof hashprefixes[prefix]) {
throw new Error("Unknown hashing prefix requested.");
}
prefix = Transaction[prefix];
prefix = hashprefixes[prefix];
} else if (!prefix) {
prefix = Transaction['HASH_TXID'];
prefix = hashprefixes['HASH_TX_ID'];
}
var hash = SerializedObject.from_json(this.tx_json).hash(prefix);

View File

@@ -3,6 +3,57 @@ var assert = require('assert');
var Transaction = utils.load_module('transaction').Transaction;
describe('Transaction', function() {
it('Serialization', function() {
var input_json = {
Account : "r4qLSAzv4LZ9TLsR7diphGwKnSEAMQTSjS",
Amount : {
currency : "LTC",
issuer : "r4qLSAzv4LZ9TLsR7diphGwKnSEAMQTSjS",
value : "9.985"
},
Destination : "r4qLSAzv4LZ9TLsR7diphGwKnSEAMQTSjS",
Fee : "15",
Flags : 0,
Paths : [
[
{
account : "rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q",
currency : "USD",
issuer : "rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q",
type : 49,
type_hex : "0000000000000031"
},
{
currency : "LTC",
issuer : "rfYv1TXnwgDDK4WQNbFALykYuEBnrR4pDX",
type : 48,
type_hex : "0000000000000030"
},
{
account : "rfYv1TXnwgDDK4WQNbFALykYuEBnrR4pDX",
currency : "LTC",
issuer : "rfYv1TXnwgDDK4WQNbFALykYuEBnrR4pDX",
type : 49,
type_hex : "0000000000000031"
}
]
],
SendMax : {
currency : "USD",
issuer : "r4qLSAzv4LZ9TLsR7diphGwKnSEAMQTSjS",
value : "30.30993068"
},
Sequence : 415,
SigningPubKey : "02854B06CE8F3E65323F89260E9E19B33DA3E01B30EA4CA172612DE77973FAC58A",
TransactionType : "Payment",
TxnSignature : "304602210096C2F385530587DE573936CA51CB86B801A28F777C944E268212BE7341440B7F022100EBF0508A9145A56CDA7FAF314DF3BBE51C6EE450BA7E74D88516891A3608644E"
};
var expected_hex = "1200002200000000240000019F61D4A3794DFA1510000000000000000000000000004C54430000000000EF7ED76B77750D79EC92A59389952E0E8054407668400000000000000F69D4CAC4AC112283000000000000000000000000005553440000000000EF7ED76B77750D79EC92A59389952E0E80544076732102854B06CE8F3E65323F89260E9E19B33DA3E01B30EA4CA172612DE77973FAC58A7448304602210096C2F385530587DE573936CA51CB86B801A28F777C944E268212BE7341440B7F022100EBF0508A9145A56CDA7FAF314DF3BBE51C6EE450BA7E74D88516891A3608644E8114EF7ED76B77750D79EC92A59389952E0E805440768314EF7ED76B77750D79EC92A59389952E0E80544076011231DD39C650A96EDA48334E70CC4A85B8B2E8502CD30000000000000000000000005553440000000000DD39C650A96EDA48334E70CC4A85B8B2E8502CD3300000000000000000000000004C5443000000000047DA9E2E00ECF224A52329793F1BB20FB1B5EA643147DA9E2E00ECF224A52329793F1BB20FB1B5EA640000000000000000000000004C5443000000000047DA9E2E00ECF224A52329793F1BB20FB1B5EA6400";
var transaction = Transaction.from_json(input_json);
assert.deepEqual(transaction.serialize().to_hex(), expected_hex);
});
it('Hashing', function() {
var input_json = {
Account : "r4qLSAzv4LZ9TLsR7diphGwKnSEAMQTSjS",