From 57b06f4cb305d618a69d6af20dfabcfd2c15db3d Mon Sep 17 00:00:00 2001 From: Cofresi Date: Tue, 18 Jun 2019 09:14:28 -0400 Subject: [PATCH] Transaction validation (#23) * typos in comment * add isValidTransaction fn * refactor validateTxProofs * add serialized merkleblocks * add tests * bump version * update package lock * rename isValidTransaction to areValidTransactions * only allow tx array as param * using MerkleBlock object as input param * use correct MerkleBlocks and fix tests * move merkleBlock creation to before hook * improve jsdoc * move merkleBlock creation to beforeEach * jsdoc: instance name instead of object * refactor validateTxProofs * more precise jsdoc param definition * test with empty transactions array * jsdoc for validateTxProofs --- lib/consensus.js | 21 ++++++++++ lib/merkleproofs.js | 20 +++++++-- package-lock.json | 20 ++++----- package.json | 2 +- test/data/merkleproofs.js | 32 ++++---------- test/index.js | 88 +++++++++++++++++++++++++++++++++++---- 6 files changed, 139 insertions(+), 44 deletions(-) diff --git a/lib/consensus.js b/lib/consensus.js index 9e9958c..54adedf 100644 --- a/lib/consensus.js +++ b/lib/consensus.js @@ -1,4 +1,5 @@ const { hasValidTarget } = require('@dashevo/dark-gravity-wave'); +const merkleProofs = require('./merkleproofs'); const utils = require('./utils'); const MIN_TIMESTAMP_HEADERS = 11; @@ -39,6 +40,26 @@ function isValidBlockHeader(newHeader, previousHeaders, network = 'mainnet') { && hasGreaterThanMedianTimestamp(newHeader, previousHeaders); } +/** + * validates an array of tx hashes or Transaction instances + * against a merkleblock and the local header chain + * @param {Transaction[]|string[]} transactions + * @param {MerkleBlock} merkleBlock - a MerkleBlock instance + * @param {SpvChain} headerChain - an instance of an SpvChain + * @return {boolean} + */ +async function areValidTransactions(transactions, merkleBlock, headerChain) { + if (!Array.isArray(transactions) || transactions.length <= 0) { + throw new Error('Please check that transactions parameter is a non-empty array'); + } + const localHeader = await headerChain.getHeader(merkleBlock.header.hash); + if (!localHeader) { + return false; + } + return merkleProofs.validateTxProofs(merkleBlock, transactions); +} + module.exports = { isValidBlockHeader, + areValidTransactions, }; diff --git a/lib/merkleproofs.js b/lib/merkleproofs.js index f68af36..d9b3a12 100644 --- a/lib/merkleproofs.js +++ b/lib/merkleproofs.js @@ -1,7 +1,21 @@ -const merkleproofs = { +const DashUtil = require('@dashevo/dash-util'); - validateTxProofs: (merkleBlock, transactions) => merkleBlock.validMerkleTree() - && transactions.filter(t => merkleBlock.hasTransaction(t)).length === transactions.length, +const merkleproofs = { + /** + * validates an array of tx hashes or Transaction instances + * against a merkleblock + * @param {MerkleBlock} merkleBlock - a MerkleBlock instance + * @param {Transaction[]|string[]} transactions + * @return {boolean} + */ + validateTxProofs: (merkleBlock, transactions) => { + let txToFilter = transactions.slice(); + if (typeof transactions[0] === 'string') { + txToFilter = txToFilter.map(tx => DashUtil.toHash(tx).toString('hex')); + } + return merkleBlock.validMerkleTree + && txToFilter.filter(tx => merkleBlock.hasTransaction(tx)).length === transactions.length; + }, }; module.exports = merkleproofs; diff --git a/package-lock.json b/package-lock.json index 8c493f6..b240215 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@dashevo/dash-spv", - "version": "1.1.5", + "version": "1.1.6", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1008,9 +1008,9 @@ } }, "levelup": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/levelup/-/levelup-4.0.1.tgz", - "integrity": "sha512-l7KXOkINXHgNqmz0v9bxvRnMCUG4gmShFrzFSZXXhcqFnfvKAW8NerVsTICpZtVhGOMAmhY6JsVoVh/tUPBmdg==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/levelup/-/levelup-4.0.2.tgz", + "integrity": "sha512-cx9PmLENwbGA3svWBEbeO2HazpOSOYSXH4VA+ahVpYyurvD+SDSfURl29VBY2qgyk+Vfy2dJd71SBRckj/EZVA==", "requires": { "deferred-leveldown": "~5.0.0", "level-errors": "~2.0.0", @@ -1183,9 +1183,9 @@ } }, "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, "mute-stream": { @@ -1752,9 +1752,9 @@ } }, "tslib": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", - "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", + "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", "dev": true }, "type-check": { diff --git a/package.json b/package.json index 31e9a61..06da7e9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@dashevo/dash-spv", - "version": "1.1.5", + "version": "1.1.6", "description": "Temporary repo until spv functions moved into dashcore-lib", "main": "index.js", "scripts": { diff --git a/test/data/merkleproofs.js b/test/data/merkleproofs.js index 9decd2c..49ea4c2 100644 --- a/test/data/merkleproofs.js +++ b/test/data/merkleproofs.js @@ -22,29 +22,15 @@ const merkleJSON = { ], flags: [219, 63], }, - mnProof: { // Mainnet Block 300100 - header: { - hash: '00000000000051b99f3fa12da6997bb54327c7b81509829467b499a7ce03136a', - version: 3, - prevHash: '0000000000154ef1f88653a6757f1182680a4d5831d815010d5fa646ae79ce35', - merkleRoot: 'a0055d45ad9b35e77fb01c59a4feb9976921493d2557a5ac0798b49e82ea1e99', - time: 1436550250, - bits: 454590659, - nonce: 2601264896, - }, - numTransactions: 12, - hashes: [ - '9d0a368bc9923c6cb966135a4ceda30cc5f259f72c8843ce015056375f8a06ec', - '39e5cd533567ac0a8602bcc4c29e2f01a4abb0fe68ffbc7be6c393db188b72e0', - 'cd75b421157eca03eff664bdc165730f91ef2fa52df19ff415ab5acb30045425', - '2ef9795147caaeecee5bc2520704bb372cde06dbd2e871750f31336fd3f02be3', - '2241d3448560f8b1d3a07ea5c31e79eb595632984a20f50944809a61fdd9fe0b', - '45afbfe270014d5593cb065562f1fed726f767fe334d8b3f4379025cfa5be8c5', - '198c03da0ccf871db91fe436e2795908eac5cc7d164232182e9445f7f9db1ab2', - 'ed07c181ce5ba7cb66d205bc970f43e1ca11996d611aa8e91e305eb8608c543c', - ], - flags: [219, 63], - }, + // merkleblock for inserted tx hash + // '7262476912a96b9a6226cfa3a8f231ba3e2b1f75c396e88367e532c79c43c95b' in testnet block 10000 + // (bloom filter: '03359be1100000000000000001') + rawMerkleBlock: '000000200e71587f863213690c24b8ad07d0753b38abe6ff1328ef7661689404000000000f1e7ce895614047d5c8d3e7b537b26b0482302301aa104947eb09c55521b316b10b1e5cff7d191c166b25b80500000002a8f781d45a5b12abffcf52bac3b3fc7824cc91ef4f9bb04bc97500a1d98d91305bc9439cc732e56783e896c3751f2b3eba31f2a8a3cf26629a6ba91269476272011d', + // merkleblock for inserted tx hashes + // '7262476912a96b9a6226cfa3a8f231ba3e2b1f75c396e88367e532c79c43c95b' + // '3f3517ee8fa95621fe8abdd81c1e0dfb50e21dd4c5a3c01eee2c47cf664821b6' in testnet block 10000 + // (bloom filter: '0797b88e7d21b31f130000000000000001') + rawMerkleBlock2: '000000200e71587f863213690c24b8ad07d0753b38abe6ff1328ef7661689404000000000f1e7ce895614047d5c8d3e7b537b26b0482302301aa104947eb09c55521b316b10b1e5cff7d191c166b25b80500000004acff07e45bfb50dd8e2fb10109f1988423cbcc9423ff07b7acb09715bd1a11e89153210725b8e219b91e433180092226c7b56649843fc51bc012c587f50030a1b6214866cf472cee1ec0a3c5d41de250fb0d1e1cd8bd8afe2156a98fee17353f5bc9439cc732e56783e896c3751f2b3eba31f2a8a3cf26629a6ba9126947627202eb01', }; module.exports = merkleJSON; diff --git a/test/index.js b/test/index.js index e4d27c0..de1a2b7 100644 --- a/test/index.js +++ b/test/index.js @@ -1,8 +1,8 @@ -const dashcore = require('@dashevo/dashcore-lib'); +const { MerkleBlock, Transaction } = require('@dashevo/dashcore-lib'); const Blockchain = require('../lib/spvchain'); const utils = require('../lib/utils'); const merkleProofs = require('../lib/merkleproofs'); - +const consensus = require('../lib/consensus'); const { testnet, testnet2, testnet3, mainnet, badRawHeaders, } = require('./data/rawHeaders'); @@ -10,6 +10,8 @@ const headers = require('./data/headers'); const merkleData = require('./data/merkleproofs'); let chain = null; +let merkleBlock = null; +let merkleBlock2 = null; require('should'); @@ -365,17 +367,89 @@ describe('Blockstore', () => { }); // TODO: -// Create scenarios where chain splits occur to form competing brances -// Difficult with current chain provided by chainmanager as this is actual hardcoded +// Create scenarios where chain splits occur to form competing branches +// Difficult with current chain provided by chainmanager as this is actually hardcoded // Dash testnet headers which requires significant CPU power to create forked chains from describe('MerkleProofs', () => { it('should validate tx inclusion in merkleblock', () => { - const merkleBlock = new dashcore.MerkleBlock(merkleData.merkleBlock); - const validTx = '45afbfe270014d5593cb065562f1fed726f767fe334d8b3f4379025cfa5be8c5'; + merkleBlock = new MerkleBlock(merkleData.merkleBlock); + const validTx = 'c5e85bfa5c0279433f8b4d33fe67f726d7fef1625506cb93554d0170e2bfaf45'; const invalidTx = `${validTx.substring(0, validTx.length - 1)}0`; - merkleProofs.validateTxProofs(merkleBlock, [validTx]).should.equal(true); merkleProofs.validateTxProofs(merkleBlock, [invalidTx]).should.equal(false); }); }); + +describe('Transaction validation', () => { + before(() => { + chain = new Blockchain('testnet', 10000, utils.normalizeHeader(testnet[0])); + chain.addHeaders(testnet.slice(1, 500)); + }); + + beforeEach(() => { + merkleBlock = new MerkleBlock(Buffer.from(merkleData.rawMerkleBlock, 'hex')); + merkleBlock2 = new MerkleBlock(Buffer.from(merkleData.rawMerkleBlock2, 'hex')); + }); + + it('should throw an error if wrong type is passed', async () => { + const validTx = '7262476912a96b9a6226cfa3a8f231ba3e2b1f75c396e88367e532c79c43c95b'; + const invalid = Buffer.from(validTx); + try { + await consensus.areValidTransactions(invalid, merkleBlock, chain); + throw new Error('Transaction validation failed to throw an error'); + } catch (e) { + e.message.should.equal('Please check that transactions parameter is a non-empty array'); + } + }); + + it('should throw an error if empty transactions array', async () => { + const transactions = []; + try { + await consensus.areValidTransactions(transactions, merkleBlock, chain); + throw new Error('Transaction validation failed to throw an error'); + } catch (e) { + e.message.should.equal('Please check that transactions parameter is a non-empty array'); + } + }); + + it('should not validate an array of raw transactions for a merkleblock that was generated with a filter containing only one of them', async () => { + const validTx = new Transaction('020000000100de7192338db34fe9bb25f34122893d94f3b43bd4c881e37924c8e95a068cc8000000006b483045022100b185b4b86b613e3ffc796db90f95dc88f82561c50ba49fa610d8090f61f38ff002201473466bddee2672ed0dba75b81c07bdade734441e005c8c7fdf12a039ff9312012102bdfedbfe6ea05de8094d18442e08c98ebd695acf489f1bdf68fe1e3aff6f488effffffff010000000000000000016a00000000'); + const validTx2 = new Transaction('0200000001ccc68ff58b7b02247f3e05440ab7fc7c8c599453de4a49e35393981890a1e984010000006b483045022100e141365c4916fa09d03aac58f215b926b777a3acec918a6becdbb03d59d28d9f02204edc1ce68d596e28e55f114c67a270302d488707e754af1261fdfc043891651c012102d5b7c0dfb2fd9591a4a98555ce806e17842f401979f2e0cc0689c91d6ca9ef87feffffff025d4c0000000000001976a9146d14b25994e4036d70eeafd4a706640337db5a5e88ac409c0000000000001976a9148b4a9da5a46c7b89b26b649bac8e34e7aa5aa63188acc2260000'); + const transactions = []; + transactions.push(validTx); + transactions.push(validTx2); + const result = await consensus.areValidTransactions(transactions, merkleBlock, chain); + result.should.equal(false); + }); + + it('should validate an array of raw transactions for a merkleblock that was generated with a filter containing both of them', async () => { + const validTx = new Transaction('020000000100de7192338db34fe9bb25f34122893d94f3b43bd4c881e37924c8e95a068cc8000000006b483045022100b185b4b86b613e3ffc796db90f95dc88f82561c50ba49fa610d8090f61f38ff002201473466bddee2672ed0dba75b81c07bdade734441e005c8c7fdf12a039ff9312012102bdfedbfe6ea05de8094d18442e08c98ebd695acf489f1bdf68fe1e3aff6f488effffffff010000000000000000016a00000000'); + const validTx2 = new Transaction('0200000001ccc68ff58b7b02247f3e05440ab7fc7c8c599453de4a49e35393981890a1e984010000006b483045022100e141365c4916fa09d03aac58f215b926b777a3acec918a6becdbb03d59d28d9f02204edc1ce68d596e28e55f114c67a270302d488707e754af1261fdfc043891651c012102d5b7c0dfb2fd9591a4a98555ce806e17842f401979f2e0cc0689c91d6ca9ef87feffffff025d4c0000000000001976a9146d14b25994e4036d70eeafd4a706640337db5a5e88ac409c0000000000001976a9148b4a9da5a46c7b89b26b649bac8e34e7aa5aa63188acc2260000'); + const transactions = []; + transactions.push(validTx); + transactions.push(validTx2); + const result = await consensus.areValidTransactions(transactions, merkleBlock2, chain); + result.should.equal(true); + }); + + it('should not validate an array of transactions hashes for a merkleblock that was generated with a filter containing only one of them', async () => { + const validTxHash = '7262476912a96b9a6226cfa3a8f231ba3e2b1f75c396e88367e532c79c43c95b'; + const validTxHash2 = '3f3517ee8fa95621fe8abdd81c1e0dfb50e21dd4c5a3c01eee2c47cf664821b6'; + const transactions = []; + transactions.push(validTxHash); + transactions.push(validTxHash2); + const result = await consensus.areValidTransactions(transactions, merkleBlock, chain); + result.should.equal(false); + }); + + it('should validate an array of transactions hashes for a merkleblock that was generated with a filter containing both of them', async () => { + const validTxHash = '7262476912a96b9a6226cfa3a8f231ba3e2b1f75c396e88367e532c79c43c95b'; + const validTxHash2 = '3f3517ee8fa95621fe8abdd81c1e0dfb50e21dd4c5a3c01eee2c47cf664821b6'; + const transactions = []; + transactions.push(validTxHash); + transactions.push(validTxHash2); + const result = await consensus.areValidTransactions(transactions, merkleBlock2, chain); + result.should.equal(true); + }); +});