diff --git a/src/offline/ledgerhash.ts b/src/offline/ledgerhash.ts index 8752d4b1..d3ecd944 100644 --- a/src/offline/ledgerhash.ts +++ b/src/offline/ledgerhash.ts @@ -28,27 +28,25 @@ function hashLedgerHeader(ledgerHeader) { function computeTransactionHash(ledger, version, options: ComputeLedgerHashOptions) { let transactions: any[] - if (options.headerOnly === undefined) { - options.headerOnly = true // by default, allow rawTransactions to be omitted - } - if (ledger.rawTransactions === undefined) { - if (options.headerOnly === true) { - return ledger.transactionHash - } else { - try { - transactions = ledger.transactions.map(tx => - JSON.parse(tx.rawTransaction)) - } catch (e) { - if (e.toString() === 'SyntaxError: Unexpected' + - ' token u in JSON at position 0') { - // one or more of the `tx.rawTransaction`s is undefined - throw new common.errors.ValidationError('ledger' - + ' is missing raw transactions') - } + if (ledger.rawTransactions) { + transactions = JSON.parse(ledger.rawTransactions) + } else if (ledger.transactions) { + try { + transactions = ledger.transactions.map(tx => + JSON.parse(tx.rawTransaction)) + } catch (e) { + if (e.toString() === 'SyntaxError: Unexpected' + + ' token u in JSON at position 0') { + // one or more of the `tx.rawTransaction`s is undefined + throw new common.errors.ValidationError('ledger' + + ' is missing raw transactions') } } } else { - transactions = JSON.parse(ledger.rawTransactions) + if (options.computeTreeHashes) + throw new common.errors.ValidationError('transactions' + + ' property is missing from the ledger') + return ledger.transactionHash } const txs = _.map(transactions, tx => { const mergeTx = _.assign({}, _.omit(tx, 'tx'), tx.tx || {}) @@ -69,8 +67,12 @@ function computeTransactionHash(ledger, version, return transactionHash } -function computeStateHash(ledger, version) { +function computeStateHash(ledger, version, + options: ComputeLedgerHashOptions) { if (ledger.rawState === undefined) { + if (options.computeTreeHashes) + throw new common.errors.ValidationError('rawState' + + ' property is missing from the ledger') return ledger.stateHash } const state = JSON.parse(ledger.rawState) @@ -85,7 +87,7 @@ function computeStateHash(ledger, version) { const sLCF_SHAMapV2 = 0x02 export type ComputeLedgerHashOptions = { - headerOnly?: boolean + computeTreeHashes?: boolean } function computeLedgerHash(ledger: any, @@ -93,7 +95,7 @@ function computeLedgerHash(ledger: any, const version = ((ledger.closeFlags & sLCF_SHAMapV2) === 0) ? 1 : 2 const subhashes = { transactionHash: computeTransactionHash(ledger, version, options), - stateHash: computeStateHash(ledger, version) + stateHash: computeStateHash(ledger, version, options) } return hashLedgerHeader(_.assign({}, ledger, subhashes)) } diff --git a/test/api-test.js b/test/api-test.js index 9c573aa3..b69dbe8f 100644 --- a/test/api-test.js +++ b/test/api-test.js @@ -2550,7 +2550,7 @@ describe('RippleAPI', function () { .then(response => { const ledger = _.assign({}, response, { parentCloseTime: response.closeTime }); - const hash = this.api.computeLedgerHash(ledger, {headerOnly: false}); + const hash = this.api.computeLedgerHash(ledger, {computeTreeHashes: true}); assert.strictEqual(hash, 'E6DB7365949BF9814D76BCC730B01818EB9136A89DB224F3F9F5AAE4569D758E'); }); @@ -2573,7 +2573,7 @@ describe('RippleAPI', function () { let hash; try { - hash = this.api.computeLedgerHash(ledger, {headerOnly: false}); + hash = this.api.computeLedgerHash(ledger, {computeTreeHashes: true}); } catch (error) { assert(error instanceof this.api.errors.ValidationError); assert.strictEqual(error.message, 'transactionHash in header does not match computed hash of transactions'); @@ -2604,7 +2604,7 @@ describe('RippleAPI', function () { let hash; try { - hash = this.api.computeLedgerHash(ledger, {headerOnly: false}); + hash = this.api.computeLedgerHash(ledger, {computeTreeHashes: true}); } catch (error) { assert(error instanceof this.api.errors.ValidationError); assert.strictEqual(error.message, 'ledger' @@ -2615,6 +2615,47 @@ describe('RippleAPI', function () { }); }); + + it('computeLedgerHash - given ledger without state or transactions - only compute ledger hash', function () { + const request = { + includeTransactions: true, + includeState: true, + includeAllData: true, + ledgerVersion: 38129 + }; + return this.api.getLedger(request).then(ledger => { + assert.strictEqual(ledger.transactions[0].rawTransaction, "{\"Account\":\"r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV\",\"Amount\":\"10000000000\",\"Destination\":\"rLQBHVhFnaC5gLEkgr6HgBJJ3bgeZHg9cj\",\"Fee\":\"10\",\"Flags\":0,\"Sequence\":62,\"SigningPubKey\":\"034AADB09CFF4A4804073701EC53C3510CDC95917C2BB0150FB742D0C66E6CEE9E\",\"TransactionType\":\"Payment\",\"TxnSignature\":\"3045022022EB32AECEF7C644C891C19F87966DF9C62B1F34BABA6BE774325E4BB8E2DD62022100A51437898C28C2B297112DF8131F2BB39EA5FE613487DDD611525F1796264639\",\"hash\":\"3B1A4E1C9BB6A7208EB146BCDB86ECEA6068ED01466D933528CA2B4C64F753EF\",\"meta\":{\"AffectedNodes\":[{\"CreatedNode\":{\"LedgerEntryType\":\"AccountRoot\",\"LedgerIndex\":\"4C6ACBD635B0F07101F7FA25871B0925F8836155462152172755845CE691C49E\",\"NewFields\":{\"Account\":\"rLQBHVhFnaC5gLEkgr6HgBJJ3bgeZHg9cj\",\"Balance\":\"10000000000\",\"Sequence\":1}}},{\"ModifiedNode\":{\"FinalFields\":{\"Account\":\"r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV\",\"Balance\":\"981481999380\",\"Flags\":0,\"OwnerCount\":0,\"Sequence\":63},\"LedgerEntryType\":\"AccountRoot\",\"LedgerIndex\":\"B33FDD5CF3445E1A7F2BE9B06336BEBD73A5E3EE885D3EF93F7E3E2992E46F1A\",\"PreviousFields\":{\"Balance\":\"991481999390\",\"Sequence\":62},\"PreviousTxnID\":\"2485FDC606352F1B0785DA5DE96FB9DBAF43EB60ECBB01B7F6FA970F512CDA5F\",\"PreviousTxnLgrSeq\":31317}}],\"TransactionIndex\":0,\"TransactionResult\":\"tesSUCCESS\"},\"ledger_index\":38129}"); + + ledger.parentCloseTime = ledger.closeTime; + + const computeLedgerHash = this.api.computeLedgerHash; + const ValidationError = this.api.errors.ValidationError + function testCompute(ledger, expectedError) { + let hash = computeLedgerHash(ledger); + assert.strictEqual(hash, + 'E6DB7365949BF9814D76BCC730B01818EB9136A89DB224F3F9F5AAE4569D758E'); + + // fail if required to compute tree hashes + try { + hash = computeLedgerHash(ledger, {computeTreeHashes: true}); + } catch (error) { + assert(error instanceof ValidationError); + assert.strictEqual(error.message, expectedError); + return; + } + assert(false, 'Should throw ValidationError instead of producing hash: ' + hash); + } + + const transactions = ledger.transactions; + delete ledger.transactions; + testCompute(ledger, 'transactions property is missing from the ledger'); + delete ledger.rawState; + testCompute(ledger, 'transactions property is missing from the ledger'); + ledger.transactions = transactions; + testCompute(ledger, 'rawState property is missing from the ledger'); + }); + }); + it('computeLedgerHash - wrong hash', function () { const request = { includeTransactions: true,