From 9a5d05f198ffa4640597d99e4f88334687e70394 Mon Sep 17 00:00:00 2001 From: Chris Clark Date: Fri, 9 Oct 2015 14:41:26 -0700 Subject: [PATCH] Decouple ledger.js and serialization code --- src/api/offline/ledgerhash.js | 4 +- src/core/ledger.js | 188 ++++++++++++++++++---------------- src/core/shamap.js | 74 ++++++------- test/ledger-test.js | 17 ++- 4 files changed, 138 insertions(+), 145 deletions(-) diff --git a/src/api/offline/ledgerhash.js b/src/api/offline/ledgerhash.js index adbdce50..474188d4 100644 --- a/src/api/offline/ledgerhash.js +++ b/src/api/offline/ledgerhash.js @@ -40,7 +40,7 @@ function computeTransactionHash(ledger) { return renameMeta; }); const ledgerObject = common.core.Ledger.from_json({transactions: txs}); - const transactionHash = ledgerObject.calc_tx_hash().to_hex(); + const transactionHash = ledgerObject.calc_tx_hash(); if (ledger.transactionHash !== undefined && ledger.transactionHash !== transactionHash) { throw new common.errors.ValidationError('transactionHash in header' @@ -55,7 +55,7 @@ function computeStateHash(ledger) { } const state = JSON.parse(ledger.rawState); const ledgerObject = common.core.Ledger.from_json({accountState: state}); - const stateHash = ledgerObject.calc_account_hash().to_hex(); + const stateHash = ledgerObject.calc_account_hash(); if (ledger.stateHash !== undefined && ledger.stateHash !== stateHash) { throw new common.errors.ValidationError('stateHash in header' + ' does not match computed hash of state'); diff --git a/src/core/ledger.js b/src/core/ledger.js index f2d9df13..35e5a334 100644 --- a/src/core/ledger.js +++ b/src/core/ledger.js @@ -1,12 +1,11 @@ 'use strict'; +const sha512half = require('./utils').sha512half; const BigNumber = require('bignumber.js'); -const Transaction = require('./transaction').Transaction; +const hashprefixes = require('./hashprefixes'); const SHAMap = require('./shamap').SHAMap; const SHAMapTreeNode = require('./shamap').SHAMapTreeNode; -const SerializedObject = require('./serializedobject').SerializedObject; -const stypes = require('./serializedtypes'); -const UInt160 = require('./uint160').UInt160; -const Currency = require('./currency').Currency; +const {decodeAddress} = require('ripple-address-codec'); +const binary = require('ripple-binary-codec'); function Ledger() { this.ledger_json = {}; @@ -20,6 +19,47 @@ Ledger.from_json = function(v) { Ledger.space = require('./ledgerspaces'); +function hash(hex) { + return sha512half(new Buffer(hex, 'hex')); +} + +function hashTransaction(txBlobHex) { + const prefix = hashprefixes.HASH_TX_ID.toString(16).toUpperCase(); + return hash(prefix + txBlobHex); +} + +function padLeftZero(string, length) { + return Array(length - string.length + 1).join('0') + string; +} + +function intToHex(integer, byteLength) { + return padLeftZero(Number(integer).toString(16), byteLength * 2); +} + +function bytesToHex(bytes) { + return (new Buffer(bytes)).toString('hex'); +} + +function bigintToHex(integerString, byteLength) { + const hex = (new BigNumber(integerString)).toString(16); + return padLeftZero(hex, byteLength * 2); +} + +function addressToHex(address) { + return (new Buffer(decodeAddress(address))).toString('hex'); +} + +function currencyToHex(currency) { + if (currency.length === 3) { + const bytes = new Array(20 + 1).join('0').split('').map(parseFloat); + bytes[12] = currency.charCodeAt(0) & 0xff; + bytes[13] = currency.charCodeAt(1) & 0xff; + bytes[14] = currency.charCodeAt(2) & 0xff; + return bytesToHex(bytes); + } + return currency; +} + /** * Generate the key for an AccountRoot entry. * @@ -27,14 +67,9 @@ Ledger.space = require('./ledgerspaces'); * @return {UInt256} */ Ledger.calcAccountRootEntryHash = -Ledger.prototype.calcAccountRootEntryHash = function(accountArg) { - const account = UInt160.from_json(accountArg); - const index = new SerializedObject(); - - index.append([0, Ledger.space.account.charCodeAt(0)]); - index.append(account.to_bytes()); - - return index.hash(); +Ledger.prototype.calcAccountRootEntryHash = function(address) { + const prefix = '00' + intToHex(Ledger.space.account.charCodeAt(0), 1); + return hash(prefix + addressToHex(address)); }; /** @@ -46,15 +81,9 @@ Ledger.prototype.calcAccountRootEntryHash = function(accountArg) { * @return {UInt256} */ Ledger.calcOfferEntryHash = -Ledger.prototype.calcOfferEntryHash = function(accountArg, sequence) { - const account = UInt160.from_json(accountArg); - const index = new SerializedObject(); - - index.append([0, Ledger.space.offer.charCodeAt(0)]); - index.append(account.to_bytes()); - stypes.Int32.serialize(index, sequence); - - return index.hash(); +Ledger.prototype.calcOfferEntryHash = function(address, sequence) { + const prefix = '00' + intToHex(Ledger.space.offer.charCodeAt(0), 1); + return hash(prefix + addressToHex(address) + intToHex(sequence, 4)); }; /** @@ -69,49 +98,47 @@ Ledger.prototype.calcOfferEntryHash = function(accountArg, sequence) { */ Ledger.calcRippleStateEntryHash = Ledger.prototype.calcRippleStateEntryHash = function( - _account1, _account2, _currency) { - const currency = Currency.from_json(_currency); - const account1 = UInt160.from_json(_account1); - const account2 = UInt160.from_json(_account2); + address1, address2, currency) { + const address1Hex = addressToHex(address1); + const address2Hex = addressToHex(address2); - 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'); - } + const swap = (new BigNumber(address1Hex, 16)).greaterThan( + new BigNumber(address2Hex, 16)); + const lowAddressHex = swap ? address2Hex : address1Hex; + const highAddressHex = swap ? address1Hex : address2Hex; - const swap = account1.greater_than(account2); - const lowAccount = swap ? account2 : account1; - const highAccount = swap ? account1 : account2; - const index = new SerializedObject(); - - index.append([0, Ledger.space.rippleState.charCodeAt(0)]); - index.append(lowAccount.to_bytes()); - index.append(highAccount.to_bytes()); - index.append(currency.to_bytes()); - - return index.hash(); + const prefix = '00' + intToHex(Ledger.space.rippleState.charCodeAt(0), 1); + return hash(prefix + lowAddressHex + highAddressHex + + currencyToHex(currency)); }; Ledger.prototype.parse_json = function(v) { this.ledger_json = v; }; +function addLengthPrefix(hex) { + const length = hex.length / 2; + if (length <= 192) { + return bytesToHex([length]) + hex; + } else if (length <= 12480) { + const x = length - 193; + return bytesToHex([193 + (x >>> 8), x & 0xff]) + hex; + } else if (length <= 918744) { + const x = length - 12481; + return bytesToHex([241 + (x >>> 16), x >>> 8 & 0xff, x & 0xff]) + hex; + } + throw new Error('Variable integer overflow.'); +} + Ledger.prototype.calc_tx_hash = function() { const tx_map = new SHAMap(); this.ledger_json.transactions.forEach(function(tx_json) { - const tx = Transaction.from_json(tx_json); - const meta = SerializedObject.from_json(tx_json.metaData); - - const data = new SerializedObject(); - stypes.VariableLength.serialize(data, tx.serialize()); - stypes.VariableLength.serialize(data, meta.to_hex()); - tx_map.add_item(tx.hash(), data, SHAMapTreeNode.TYPE_TRANSACTION_MD); + const txBlobHex = binary.encode(tx_json); + const metaHex = binary.encode(tx_json.metaData); + const txHash = hashTransaction(txBlobHex); + const data = addLengthPrefix(txBlobHex) + addLengthPrefix(metaHex); + tx_map.add_item(txHash, data, SHAMapTreeNode.TYPE_TRANSACTION_MD); }); return tx_map.hash(); @@ -127,54 +154,33 @@ Ledger.prototype.calc_tx_hash = function() { * * @return {UInt256} - hash of shamap */ -Ledger.prototype.calc_account_hash = function(options) { +Ledger.prototype.calc_account_hash = function() { const account_map = new SHAMap(); - let erred; - this.ledger_json.accountState.forEach(function(le) { - let data = SerializedObject.from_json(le); - - let json; - if (options && options.sanity_test) { - try { - json = data.to_json(); - data = SerializedObject.from_json(json); - } catch (e) { - console.log('account state item: ', le); - console.log('to_json() ', json); - console.log('exception: ', e); - erred = true; - } - } - - account_map.add_item(le.index, data, SHAMapTreeNode.TYPE_ACCOUNT_STATE); + this.ledger_json.accountState.forEach(function(ledgerEntry) { + const data = binary.encode(ledgerEntry); + account_map.add_item(ledgerEntry.index, data, + SHAMapTreeNode.TYPE_ACCOUNT_STATE); }); - if (erred) { - throw new Error('There were errors with sanity_test'); // all logged above - } - return account_map.hash(); }; // see rippled Ledger::updateHash() Ledger.calculateLedgerHash = Ledger.prototype.calculateLedgerHash = function(ledgerHeader) { - const so = new SerializedObject(); - const prefix = 0x4C575200; - const totalCoins = (new BigNumber(ledgerHeader.total_coins)).toString(16); - - stypes.Int32.serialize(so, Number(ledgerHeader.ledger_index)); - stypes.Int64.serialize(so, totalCoins); - stypes.Hash256.serialize(so, ledgerHeader.parent_hash); - stypes.Hash256.serialize(so, ledgerHeader.transaction_hash); - stypes.Hash256.serialize(so, ledgerHeader.account_hash); - stypes.Int32.serialize(so, ledgerHeader.parent_close_time); - stypes.Int32.serialize(so, ledgerHeader.close_time); - stypes.Int8.serialize(so, ledgerHeader.close_time_resolution); - stypes.Int8.serialize(so, ledgerHeader.close_flags); - - return so.hash(prefix).to_hex(); + const prefix = '4C575200'; + return hash(prefix + + intToHex(ledgerHeader.ledger_index, 4) + + bigintToHex(ledgerHeader.total_coins, 8) + + ledgerHeader.parent_hash + + ledgerHeader.transaction_hash + + ledgerHeader.account_hash + + intToHex(ledgerHeader.parent_close_time, 4) + + intToHex(ledgerHeader.close_time, 4) + + intToHex(ledgerHeader.close_time_resolution, 1) + + intToHex(ledgerHeader.close_flags, 1) + ); }; exports.Ledger = Ledger; diff --git a/src/core/shamap.js b/src/core/shamap.js index 7abd34da..0db5d9c5 100644 --- a/src/core/shamap.js +++ b/src/core/shamap.js @@ -1,10 +1,10 @@ 'use strict'; -var util = require('util'); -var hashprefixes = require('./hashprefixes'); - -var UInt256 = require('./uint256').UInt256; -var SerializedObject = require('./serializedobject').SerializedObject; +const util = require('util'); +const hashprefixes = require('./hashprefixes'); +const sha512half = require('./utils').sha512half; +const HEX_ZERO = '00000000000000000000000000000000' + + '00000000000000000000000000000000'; /** * Abstract class representing a node in a SHAMap tree. @@ -20,18 +20,22 @@ SHAMapTreeNode.TYPE_TRANSACTION_NM = 2; SHAMapTreeNode.TYPE_TRANSACTION_MD = 3; SHAMapTreeNode.TYPE_ACCOUNT_STATE = 4; +function hash(hex) { + return sha512half(new Buffer(hex, 'hex')); +} + /** * @param {String} tag (64 hexadecimal characters) * @param {SHAMapTreeNode} node * @return {void} * @virtual */ -/*eslint-disable no-unused-vars*/ +/* eslint-disable no-unused-vars*/ SHAMapTreeNode.prototype.add_item = function(tag, node) { throw new Error( 'Called unimplemented virtual method SHAMapTreeNode#add_item.'); }; -/*eslint-enable no-unused-vars*/ +/* eslint-enable no-unused-vars*/ SHAMapTreeNode.prototype.hash = function() { throw new Error('Called unimplemented virtual method SHAMapTreeNode#hash.'); @@ -44,12 +48,9 @@ SHAMapTreeNode.prototype.hash = function() { */ function SHAMapTreeNodeInner(depth) { SHAMapTreeNode.call(this); - this.leaves = {}; - this.type = SHAMapTreeNode.INNER; this.depth = depth === undefined ? 0 : depth; - this.empty = true; } @@ -61,8 +62,8 @@ util.inherits(SHAMapTreeNodeInner, SHAMapTreeNode); * @return {void} */ SHAMapTreeNodeInner.prototype.add_item = function(tag, node) { - var depth = this.depth; - var existing_node = this.get_node(tag[depth]); + const depth = this.depth; + const existing_node = this.get_node(tag[depth]); if (existing_node) { // A node already exists in this slot @@ -75,7 +76,7 @@ SHAMapTreeNodeInner.prototype.add_item = function(tag, node) { '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(depth + 1); + const new_inner_node = new SHAMapTreeNodeInner(depth + 1); // Parent new and existing node new_inner_node.add_item(existing_node.tag, existing_node); @@ -107,35 +108,27 @@ SHAMapTreeNodeInner.prototype.get_node = function(slot) { SHAMapTreeNodeInner.prototype.hash = function() { if (this.empty) { - return UInt256.from_hex(UInt256.HEX_ZERO); + return HEX_ZERO; } - var hash_buffer = new SerializedObject(); - - for (var i = 0; i < 16; i++) { - var leafHash = UInt256.from_hex(UInt256.HEX_ZERO); - var slot = i.toString(16).toUpperCase(); - - if (typeof this.leaves[slot] === 'object') { - leafHash = this.leaves[slot].hash(); - } - - hash_buffer.append(leafHash.to_bytes()); + let hex = ''; + for (let i = 0; i < 16; i++) { + const slot = i.toString(16).toUpperCase(); + hex += this.leaves[slot] ? this.leaves[slot].hash() : HEX_ZERO; } - var hash = hash_buffer.hash(hashprefixes.HASH_INNER_NODE); - - return UInt256.from_bits(hash); + const prefix = hashprefixes.HASH_INNER_NODE.toString(16); + return hash(prefix + hex); }; /** * Leaf node in a SHAMap tree. * @param {String} tag (equates to a ledger entry `index`) - * @param {SerializedObject} node (bytes of account state, transaction etc) + * @param {String} data (hex of account state, transaction etc) * @param {Number} type (one of TYPE_ACCOUNT_STATE, TYPE_TRANSACTION_MD etc) * @class */ -function SHAMapTreeNodeLeaf(tag, node, type) { +function SHAMapTreeNodeLeaf(tag, data, type) { SHAMapTreeNode.call(this); if (typeof tag !== 'string') { @@ -143,26 +136,22 @@ function SHAMapTreeNodeLeaf(tag, node, type) { } this.tag = tag; - this.tag_bytes = UInt256.from_hex(this.tag).to_bytes(); this.type = type; - this.node = node; + this.data = data; } util.inherits(SHAMapTreeNodeLeaf, SHAMapTreeNode); SHAMapTreeNodeLeaf.prototype.hash = function() { - var buffer = new SerializedObject(); switch (this.type) { case SHAMapTreeNode.TYPE_ACCOUNT_STATE: - buffer.append(this.node); - buffer.append(this.tag_bytes); - return buffer.hash(hashprefixes.HASH_LEAF_NODE); + const leafPrefix = hashprefixes.HASH_LEAF_NODE.toString(16); + return hash(leafPrefix + this.data + this.tag); case SHAMapTreeNode.TYPE_TRANSACTION_NM: - return this.tag_bytes; + return this.tag; case SHAMapTreeNode.TYPE_TRANSACTION_MD: - buffer.append(this.node); - buffer.append(this.tag_bytes); - return buffer.hash(hashprefixes.HASH_TX_NODE); + const txPrefix = hashprefixes.HASH_TX_NODE.toString(16); + return hash(txPrefix + this.data + this.tag); default: throw new Error('Tried to hash a SHAMap node of unknown type.'); } @@ -172,9 +161,8 @@ function SHAMap() { this.root = new SHAMapTreeNodeInner(0); } -SHAMap.prototype.add_item = function(tag, node, type) { - node = new SHAMapTreeNodeLeaf(tag, node, type); - this.root.add_item(tag, node); +SHAMap.prototype.add_item = function(tag, data, type) { + this.root.add_item(tag, new SHAMapTreeNodeLeaf(tag, data, type)); }; SHAMap.prototype.hash = function() { diff --git a/test/ledger-test.js b/test/ledger-test.js index 997e7911..f768737f 100644 --- a/test/ledger-test.js +++ b/test/ledger-test.js @@ -23,13 +23,12 @@ function create_ledger_test(ledger_index) { if (hasAccounts) { it('has account_hash of ' + ledger_json.account_hash, function() { - assert.equal(ledger_json.account_hash, - ledger.calc_account_hash({sanity_test: true}).to_hex()); + assert.equal(ledger_json.account_hash, ledger.calc_account_hash()); }); } it('has transaction_hash of ' + ledger_json.transaction_hash, function() { assert.equal(ledger_json.transaction_hash, - ledger.calc_tx_hash().to_hex()); + ledger.calc_tx_hash()); }); }); } @@ -47,7 +46,7 @@ describe('Ledger', function() { const expectedEntryHash = '2B6AC232AA4C4BE41BF49D2459FA4A0347E1B543A4C92FCEE0821C0201E2E9A8'; const actualEntryHash = Ledger.calcAccountRootEntryHash(account); - assert.equal(actualEntryHash.to_hex(), expectedEntryHash); + assert.equal(actualEntryHash, expectedEntryHash); }); }); @@ -61,8 +60,8 @@ describe('Ledger', function() { const actualEntryHash1 = Ledger.calcRippleStateEntryHash(account1, account2, currency); const actualEntryHash2 = Ledger.calcRippleStateEntryHash(account2, account1, currency); - assert.equal(actualEntryHash1.to_hex(), expectedEntryHash); - assert.equal(actualEntryHash2.to_hex(), expectedEntryHash); + assert.equal(actualEntryHash1, expectedEntryHash); + assert.equal(actualEntryHash2, expectedEntryHash); }); it('will calculate the RippleState entry hash for r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV and rUAMuQTfVhbfqUDuro7zzy4jj4Wq57MPTj in UAM', function() { @@ -74,8 +73,8 @@ describe('Ledger', function() { const actualEntryHash1 = Ledger.calcRippleStateEntryHash(account1, account2, currency); const actualEntryHash2 = Ledger.calcRippleStateEntryHash(account2, account1, currency); - assert.equal(actualEntryHash1.to_hex(), expectedEntryHash); - assert.equal(actualEntryHash2.to_hex(), expectedEntryHash); + assert.equal(actualEntryHash1, expectedEntryHash); + assert.equal(actualEntryHash2, expectedEntryHash); }); }); @@ -86,7 +85,7 @@ describe('Ledger', function() { const expectedEntryHash = '03F0AED09DEEE74CEF85CD57A0429D6113507CF759C597BABB4ADB752F734CE3'; const actualEntryHash = Ledger.calcOfferEntryHash(account, sequence); - assert.equal(actualEntryHash.to_hex(), expectedEntryHash); + assert.equal(actualEntryHash, expectedEntryHash); }); }); });