From 86b23f37adcfc7aa7ab9228f7ae4239187c10843 Mon Sep 17 00:00:00 2001 From: Larry Ruane Date: Wed, 7 Aug 2019 14:23:42 -0600 Subject: [PATCH] add spentindex RPC for bitcore block explorer --- qa/pull-tester/rpc-tests.sh | 1 + qa/rpc-tests/addressindex.py | 5 + qa/rpc-tests/getrawtransaction_insight.py | 9 +- qa/rpc-tests/spentindex.py | 182 ++++++++++++++++++++++ src/main.cpp | 7 + src/rpc/blockchain.cpp | 175 +++++++++++++++++++++ src/rpc/client.cpp | 1 + src/rpc/misc.cpp | 98 ++++++++++-- src/rpc/rawtransaction.cpp | 1 + src/test/rpc_tests.cpp | 17 ++ src/txmempool.cpp | 50 ++++++ src/txmempool.h | 10 ++ 12 files changed, 537 insertions(+), 19 deletions(-) create mode 100755 qa/rpc-tests/spentindex.py diff --git a/qa/pull-tester/rpc-tests.sh b/qa/pull-tester/rpc-tests.sh index 9148a6a8a..b5c3f7087 100755 --- a/qa/pull-tester/rpc-tests.sh +++ b/qa/pull-tester/rpc-tests.sh @@ -58,6 +58,7 @@ testScripts=( 'nodehandling.py' 'reindex.py' 'addressindex.py' + 'spentindex.py' 'decodescript.py' 'blockchain.py' 'disablewallet.py' diff --git a/qa/rpc-tests/addressindex.py b/qa/rpc-tests/addressindex.py index 4e18d9514..f924f0875 100755 --- a/qa/rpc-tests/addressindex.py +++ b/qa/rpc-tests/addressindex.py @@ -202,6 +202,11 @@ class AddressIndexTest(BitcoinTestFramework): # a single address can be specified as a string (not json object) assert_equal([mempool[1]], self.nodes[0].getaddressmempool(addr1)) + tx = self.nodes[0].getrawtransaction(txid, 1) + assert_equal(tx['vin'][0]['address'], addr1) + assert_equal(tx['vin'][0]['value'], 4) + assert_equal(tx['vin'][0]['valueSat'], 4 * COIN) + txids_a1.append(txid) expected_deltas.append({ 'height': 111, diff --git a/qa/rpc-tests/getrawtransaction_insight.py b/qa/rpc-tests/getrawtransaction_insight.py index 85f39ccd7..620f86bfc 100755 --- a/qa/rpc-tests/getrawtransaction_insight.py +++ b/qa/rpc-tests/getrawtransaction_insight.py @@ -64,10 +64,11 @@ class GetrawtransactionTest(BitcoinTestFramework): self.sync_all() tx_a = self.nodes[2].getrawtransaction(txid_a, 1) - # txid_b is not yet confirmed, so these should not be set - assert('spentTxId' not in tx_a['vout'][0]) - assert('spentIndex' not in tx_a['vout'][0]) - assert('spentHeight' not in tx_a['vout'][0]) + # txid_b is not yet confirmed, so height is invalid (-1) + vout = filter(lambda o: o['value'] == 2, tx_a['vout']) + assert_equal(vout[0]['spentTxId'], txid_b) + assert_equal(vout[0]['spentIndex'], 0) + assert_equal(vout[0]['spentHeight'], -1) # confirm txid_b (a to b transaction) self.nodes[0].generate(1) diff --git a/qa/rpc-tests/spentindex.py b/qa/rpc-tests/spentindex.py new file mode 100755 index 000000000..28e626573 --- /dev/null +++ b/qa/rpc-tests/spentindex.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python +# Copyright (c) 2019 The Zcash developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +# +# Test spentindex generation and fetching +# + +import sys; assert sys.version_info < (3,), ur"This script does not run under Python 3. Please use Python 2.7.x." + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.authproxy import JSONRPCException + +from test_framework.util import assert_equal +from test_framework.util import initialize_chain_clean +from test_framework.util import start_nodes, stop_nodes, connect_nodes +from test_framework.util import wait_bitcoinds +from test_framework.util import fail + +from test_framework.mininode import COIN + +class SpentIndexTest(BitcoinTestFramework): + + def setup_chain(self): + print("Initializing test directory "+self.options.tmpdir) + initialize_chain_clean(self.options.tmpdir, 3) + + def setup_network(self): + # -insightexplorer causes spentindex to be enabled (fSpentIndex = true) + + self.nodes = start_nodes(3, self.options.tmpdir, + [['-debug', '-txindex', '-experimentalfeatures', '-insightexplorer']]*3) + connect_nodes(self.nodes[0], 1) + connect_nodes(self.nodes[0], 2) + + self.is_network_split = False + self.sync_all() + + def run_test(self): + self.nodes[0].generate(105) + self.sync_all() + + chain_height = self.nodes[1].getblockcount() + assert_equal(chain_height, 105) + + # Test getrawtransaction changes and the getspentinfo RPC + + # send coinbase to address addr1 + addr1 = self.nodes[1].getnewaddress() + txid1 = self.nodes[0].sendtoaddress(addr1, 2) + self.sync_all() + block_hash1 = self.nodes[0].generate(1) + self.sync_all() + + # send from addr1 to addr2 + # (the only utxo on node 1 is from address addr1) + addr2 = self.nodes[2].getnewaddress() + txid2 = self.nodes[1].sendtoaddress(addr2, 1) + self.sync_all() + + # addr1 to addr2 transaction is not confirmed, so it has no height + tx2 = self.nodes[2].getrawtransaction(txid2, 1) + assert('height' not in tx2) + + # confirm addr1 to addr2 transaction + block_hash2 = self.nodes[0].generate(1) + self.sync_all() + + # Restart all nodes to ensure index files are saved to disk and recovered + stop_nodes(self.nodes) + wait_bitcoinds() + self.setup_network() + + # Check new fields added to getrawtransaction + tx1 = self.nodes[2].getrawtransaction(txid1, 1) + assert_equal(tx1['vin'][0]['value'], 10) # coinbase + assert_equal(tx1['vin'][0]['valueSat'], 10*COIN) + # we want the non-change (payment) output + vout = filter(lambda o: o['value'] == 2, tx1['vout']) + n = vout[0]['n'] + assert_equal(vout[0]['spentTxId'], txid2) + assert_equal(vout[0]['spentIndex'], 0) + assert_equal(vout[0]['spentHeight'], 107) + assert_equal(tx1['height'], 106) + + tx2 = self.nodes[2].getrawtransaction(txid2, 1) + assert_equal(tx2['vin'][0]['address'], addr1) + assert_equal(tx2['vin'][0]['value'], 2) + assert_equal(tx2['vin'][0]['valueSat'], 2*COIN) + # since this transaction's outputs haven't yet been + # spent, these fields should not be present + assert('spentTxId' not in tx2['vout'][0]) + assert('spentIndex' not in tx2['vout'][0]) + assert('spentHeight' not in tx2['vout'][0]) + assert_equal(tx2['height'], 107) + + # Given a transaction output, getspentinfo() returns a reference + # to the (later, confirmed) transaction that spent that output, + # that is, the transaction that used this output as an input. + spentinfo = self.nodes[2].getspentinfo({'txid': txid1, 'index': n}) + assert_equal(spentinfo['height'], 107) + assert_equal(spentinfo['index'], 0) + assert_equal(spentinfo['txid'], txid2) + + # specifying an output that hasn't been spent should fail + try: + self.nodes[1].getspentinfo({'txid': txid2, 'index': 0}) + fail('getspentinfo should have thrown an exception') + except JSONRPCException, e: + assert_equal(e.error['message'], "Unable to get spent info") + + block_hash_next = self.nodes[0].generate(1) + self.sync_all() + + # Test the getblockdeltas RPC + blockdeltas = self.nodes[2].getblockdeltas(block_hash1[0]) + assert_equal(blockdeltas['confirmations'], 3) + assert_equal(blockdeltas['height'], 106) + assert_equal(blockdeltas['version'], 4) + assert_equal(blockdeltas['hash'], block_hash1[0]) + assert_equal(blockdeltas['nextblockhash'], block_hash2[0]) + deltas = blockdeltas['deltas'] + # block contains two transactions, coinbase, and earlier coinbase to addr1 + assert_equal(len(deltas), 2) + coinbase_tx = deltas[0] + assert_equal(coinbase_tx['index'], 0) + assert_equal(len(coinbase_tx['inputs']), 0) + assert_equal(len(coinbase_tx['outputs']), 2) + assert_equal(coinbase_tx['outputs'][0]['index'], 0) + assert_equal(coinbase_tx['outputs'][1]['index'], 1) + assert_equal(coinbase_tx['outputs'][1]['satoshis'], 2.5*COIN) + + to_a_tx = deltas[1] + assert_equal(to_a_tx['index'], 1) + assert_equal(to_a_tx['txid'], txid1) + + assert_equal(len(to_a_tx['inputs']), 1) + assert_equal(to_a_tx['inputs'][0]['index'], 0) + assert_equal(to_a_tx['inputs'][0]['prevout'], 0) + assert_equal(to_a_tx['inputs'][0]['satoshis'], -10*COIN) + + assert_equal(len(to_a_tx['outputs']), 2) + # find the nonchange output, which is the payment to addr1 + out = filter(lambda o: o['satoshis'] == 2*COIN, to_a_tx['outputs']) + assert_equal(len(out), 1) + assert_equal(out[0]['address'], addr1) + + blockdeltas = self.nodes[2].getblockdeltas(block_hash2[0]) + assert_equal(blockdeltas['confirmations'], 2) + assert_equal(blockdeltas['height'], 107) + assert_equal(blockdeltas['version'], 4) + assert_equal(blockdeltas['hash'], block_hash2[0]) + assert_equal(blockdeltas['previousblockhash'], block_hash1[0]) + assert_equal(blockdeltas['nextblockhash'], block_hash_next[0]) + deltas = blockdeltas['deltas'] + assert_equal(len(deltas), 2) + coinbase_tx = deltas[0] + assert_equal(coinbase_tx['index'], 0) + assert_equal(len(coinbase_tx['inputs']), 0) + assert_equal(len(coinbase_tx['outputs']), 2) + assert_equal(coinbase_tx['outputs'][0]['index'], 0) + assert_equal(coinbase_tx['outputs'][1]['index'], 1) + assert_equal(coinbase_tx['outputs'][1]['satoshis'], 2.5*COIN) + + to_b_tx = deltas[1] + assert_equal(to_b_tx['index'], 1) + assert_equal(to_b_tx['txid'], txid2) + + assert_equal(len(to_b_tx['inputs']), 1) + assert_equal(to_b_tx['inputs'][0]['index'], 0) + assert_equal(to_b_tx['inputs'][0]['prevtxid'], txid1) + assert_equal(to_b_tx['inputs'][0]['satoshis'], -2*COIN) + + assert_equal(len(to_b_tx['outputs']), 2) + # find the nonchange output, which is the payment to addr2 + out = filter(lambda o: o['satoshis'] == 1*COIN, to_b_tx['outputs']) + assert_equal(len(out), 1) + assert_equal(out[0]['address'], addr2) + +if __name__ == '__main__': + SpentIndexTest().main() diff --git a/src/main.cpp b/src/main.cpp index c0cd9b043..793b824d6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1604,6 +1604,11 @@ bool AcceptToMemoryPool(CTxMemPool& pool, CValidationState &state, const CTransa if (fAddressIndex) { pool.addAddressIndex(entry, view); } + + // insightexplorer: Add memory spent index + if (fSpentIndex) { + pool.addSpentIndex(entry, view); + } } SyncWithWallets(tx, NULL); @@ -1616,6 +1621,8 @@ bool GetSpentIndex(CSpentIndexKey &key, CSpentIndexValue &value) AssertLockHeld(cs_main); if (!fSpentIndex) return false; + if (mempool.getSpentIndex(key, value)) + return true; return pblocktree->ReadSpentIndex(key, value); } diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index 580ba8b11..86f01d9bd 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -8,6 +8,7 @@ #include "chainparams.h" #include "checkpoints.h" #include "consensus/validation.h" +#include "key_io.h" #include "main.h" #include "primitives/transaction.h" #include "rpc/server.h" @@ -125,6 +126,94 @@ UniValue blockheaderToJSON(const CBlockIndex* blockindex) return result; } +// insightexplorer +UniValue blockToDeltasJSON(const CBlock& block, const CBlockIndex* blockindex) +{ + UniValue result(UniValue::VOBJ); + result.push_back(Pair("hash", block.GetHash().GetHex())); + // Only report confirmations if the block is on the main chain + if (!chainActive.Contains(blockindex)) + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block is an orphan"); + int confirmations = chainActive.Height() - blockindex->nHeight + 1; + result.push_back(Pair("confirmations", confirmations)); + result.push_back(Pair("size", (int)::GetSerializeSize(block, SER_NETWORK, PROTOCOL_VERSION))); + result.push_back(Pair("height", blockindex->nHeight)); + result.push_back(Pair("version", block.nVersion)); + result.push_back(Pair("merkleroot", block.hashMerkleRoot.GetHex())); + + UniValue deltas(UniValue::VARR); + for (unsigned int i = 0; i < block.vtx.size(); i++) { + const CTransaction &tx = block.vtx[i]; + const uint256 txhash = tx.GetHash(); + + UniValue entry(UniValue::VOBJ); + entry.push_back(Pair("txid", txhash.GetHex())); + entry.push_back(Pair("index", (int)i)); + + UniValue inputs(UniValue::VARR); + if (!tx.IsCoinBase()) { + for (size_t j = 0; j < tx.vin.size(); j++) { + const CTxIn input = tx.vin[j]; + UniValue delta(UniValue::VOBJ); + CSpentIndexValue spentInfo; + CSpentIndexKey spentKey(input.prevout.hash, input.prevout.n); + + if (!GetSpentIndex(spentKey, spentInfo)) { + throw JSONRPCError(RPC_INTERNAL_ERROR, "Spent information not available"); + } + CTxDestination dest = DestFromAddressHash(spentInfo.addressType, spentInfo.addressHash); + if (IsValidDestination(dest)) { + delta.push_back(Pair("address", EncodeDestination(dest))); + } + delta.push_back(Pair("satoshis", -1 * spentInfo.satoshis)); + delta.push_back(Pair("index", (int)j)); + delta.push_back(Pair("prevtxid", input.prevout.hash.GetHex())); + delta.push_back(Pair("prevout", (int)input.prevout.n)); + + inputs.push_back(delta); + } + } + entry.push_back(Pair("inputs", inputs)); + + UniValue outputs(UniValue::VARR); + for (unsigned int k = 0; k < tx.vout.size(); k++) { + const CTxOut &out = tx.vout[k]; + UniValue delta(UniValue::VOBJ); + const uint160 addrhash = out.scriptPubKey.AddressHash(); + CTxDestination dest; + + if (out.scriptPubKey.IsPayToScriptHash()) { + dest = CScriptID(addrhash); + } else if (out.scriptPubKey.IsPayToPublicKeyHash()) { + dest = CKeyID(addrhash); + } + if (IsValidDestination(dest)) { + delta.push_back(Pair("address", EncodeDestination(dest))); + } + delta.push_back(Pair("satoshis", out.nValue)); + delta.push_back(Pair("index", (int)k)); + + outputs.push_back(delta); + } + entry.push_back(Pair("outputs", outputs)); + deltas.push_back(entry); + } + result.push_back(Pair("deltas", deltas)); + result.push_back(Pair("time", block.GetBlockTime())); + result.push_back(Pair("mediantime", (int64_t)blockindex->GetMedianTimePast())); + result.push_back(Pair("nonce", block.nNonce.GetHex())); + result.push_back(Pair("bits", strprintf("%08x", block.nBits))); + result.push_back(Pair("difficulty", GetDifficulty(blockindex))); + result.push_back(Pair("chainwork", blockindex->nChainWork.GetHex())); + + if (blockindex->pprev) + result.push_back(Pair("previousblockhash", blockindex->pprev->GetBlockHash().GetHex())); + CBlockIndex *pnext = chainActive.Next(blockindex); + if (pnext) + result.push_back(Pair("nextblockhash", pnext->GetBlockHash().GetHex())); + return result; +} + UniValue blockToJSON(const CBlock& block, const CBlockIndex* blockindex, bool txDetails = false) { UniValue result(UniValue::VOBJ); @@ -313,6 +402,89 @@ UniValue getrawmempool(const UniValue& params, bool fHelp) return mempoolToJSON(fVerbose); } +// insightexplorer +UniValue getblockdeltas(const UniValue& params, bool fHelp) +{ + std::string enableArg = "insightexplorer"; + bool enabled = fExperimentalMode && fInsightExplorer; + std::string disabledMsg = ""; + if (!enabled) { + disabledMsg = experimentalDisabledHelpMsg("getblockdeltas", enableArg); + } + if (fHelp || params.size() != 1) + throw runtime_error( + "getblockdeltas \"blockhash\"\n" + "\nReturns the txid and index where an output is spent.\n" + + disabledMsg + + "\nArguments:\n" + "1. \"hash\" (string, required) The block hash\n" + "\nResult:\n" + "{\n" + " \"hash\": \"hash\", (string) block ID\n" + " \"confirmations\": n, (numeric) number of confirmations\n" + " \"size\": n, (numeric) block size in bytes\n" + " \"height\": n, (numeric) block height\n" + " \"version\": n, (numeric) block version (e.g. 4)\n" + " \"merkleroot\": \"hash\", (string) block Merkle root\n" + " \"deltas\": [\n" + " {\n" + " \"txid\": \"hash\", (string) transaction ID\n" + " \"index\": n, (numeric) tx index in block\n" + " \"inputs\": [\n" + " {\n" + " \"address\": \"taddr\", (string) transparent address\n" + " \"satoshis\": n, (numeric) negative of spend amount\n" + " \"index\": n, (numeric) vin index\n" + " \"prevtxid\": \"hash\", (string) source utxo tx ID\n" + " \"prevout\": n (numeric) source utxo index\n" + " }, ...\n" + " ],\n" + " \"outputs\": [\n" + " {\n" + " \"address\": \"taddr\", (string) transparent address\n" + " \"satoshis\": n, (numeric) amount\n" + " \"index\": n (numeric) vout index\n" + " }, ...\n" + " ]\n" + " }, ...\n" + " ],\n" + " \"time\": n,\n" + " \"mediantime\": n,\n" + " \"nonce\": \"hexstring\",\n" + " \"bits\": \"hexstring\",\n" + " \"difficulty\": ,\n" + " \"chainwork\": \"hexstring\",\n" + " \"previousblockhash\": \"hash\",\n" + " \"nextblockhash\": \"hash\"\n" + "}\n" + "\nExamples:\n" + + HelpExampleCli("getblockdeltas", "00227e566682aebd6a7a5b772c96d7a999cadaebeaf1ce96f4191a3aad58b00b") + + HelpExampleRpc("getblockdeltas", "\"00227e566682aebd6a7a5b772c96d7a999cadaebeaf1ce96f4191a3aad58b00b\"") + ); + + if (!enabled) { + throw JSONRPCError(RPC_MISC_ERROR, "Error: getblockdeltas is disabled. " + "Run './zcash-cli help getblockdeltas' for instructions on how to enable this feature."); + } + + std::string strHash = params[0].get_str(); + uint256 hash(uint256S(strHash)); + + if (mapBlockIndex.count(hash) == 0) + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found"); + + CBlock block; + CBlockIndex* pblockindex = mapBlockIndex[hash]; + + if (fHavePruned && !(pblockindex->nStatus & BLOCK_HAVE_DATA) && pblockindex->nTx > 0) + throw JSONRPCError(RPC_INTERNAL_ERROR, "Block not available (pruned data)"); + + if (!ReadBlockFromDisk(block, pblockindex, Params().GetConsensus())) + throw JSONRPCError(RPC_INTERNAL_ERROR, "Can't read block from disk"); + + return blockToDeltasJSON(block, pblockindex); +} + UniValue getblockhash(const UniValue& params, bool fHelp) { if (fHelp || params.size() != 1) @@ -1046,6 +1218,9 @@ static const CRPCCommand commands[] = { "blockchain", "gettxoutsetinfo", &gettxoutsetinfo, true }, { "blockchain", "verifychain", &verifychain, true }, + // insightexplorer + { "blockchain", "getblockdeltas", &getblockdeltas, false }, + /* Not shown in help */ { "hidden", "invalidateblock", &invalidateblock, true }, { "hidden", "reconsiderblock", &reconsiderblock, true }, diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 137210a23..8ba3c8515 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -99,6 +99,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "prioritisetransaction", 2 }, { "setban", 2 }, { "setban", 3 }, + { "getspentinfo", 0}, { "getaddresstxids", 0}, { "getaddressbalance", 0}, { "getaddressdeltas", 0}, diff --git a/src/rpc/misc.cpp b/src/rpc/misc.cpp index a34d7b250..84a2b47a1 100644 --- a/src/rpc/misc.cpp +++ b/src/rpc/misc.cpp @@ -484,6 +484,7 @@ UniValue setmocktime(const UniValue& params, bool fHelp) return NullUniValue; } +// insightexplorer static bool getAddressFromIndex( int type, const uint160 &hash, std::string &address) { @@ -520,6 +521,7 @@ static bool getIndexKey( return false; } +// insightexplorer static bool getAddressesFromParams( const UniValue& params, std::vector> &addresses) @@ -552,12 +554,13 @@ static bool getAddressesFromParams( return true; } +// insightexplorer UniValue getaddressmempool(const UniValue& params, bool fHelp) { std::string enableArg = "insightexplorer"; - bool fEnableGetAddressMempool = fExperimentalMode && fInsightExplorer; + bool enabled = fExperimentalMode && fInsightExplorer; std::string disabledMsg = ""; - if (!fEnableGetAddressMempool) { + if (!enabled) { disabledMsg = experimentalDisabledHelpMsg("getaddressmempool", enableArg); } if (fHelp || params.size() != 1) @@ -592,7 +595,7 @@ UniValue getaddressmempool(const UniValue& params, bool fHelp) + HelpExampleRpc("getaddressmempool", "{\"addresses\": [\"tmYXBYJj1K7vhejSec5osXK2QsGa5MTisUQ\"]}") ); - if (!fEnableGetAddressMempool) { + if (!enabled) { throw JSONRPCError(RPC_MISC_ERROR, "Error: getaddressmempool is disabled. " "Run './zcash-cli help getaddressmempool' for instructions on how to enable this feature."); } @@ -632,12 +635,13 @@ UniValue getaddressmempool(const UniValue& params, bool fHelp) return result; } +// insightexplorer UniValue getaddressutxos(const UniValue& params, bool fHelp) { std::string enableArg = "insightexplorer"; - bool fEnableGetAddressUtxos = fExperimentalMode && fInsightExplorer; + bool enabled = fExperimentalMode && fInsightExplorer; std::string disabledMsg = ""; - if (!fEnableGetAddressUtxos) { + if (!enabled) { disabledMsg = experimentalDisabledHelpMsg("getaddressutxos", enableArg); } if (fHelp || params.size() != 1) @@ -688,7 +692,7 @@ UniValue getaddressutxos(const UniValue& params, bool fHelp) + HelpExampleRpc("getaddressutxos", "{\"addresses\": [\"tmYXBYJj1K7vhejSec5osXK2QsGa5MTisUQ\"], \"chainInfo\": true}") ); - if (!fEnableGetAddressUtxos) { + if (!enabled) { throw JSONRPCError(RPC_MISC_ERROR, "Error: getaddressutxos is disabled. " "Run './zcash-cli help getaddressutxos' for instructions on how to enable this feature."); } @@ -790,12 +794,13 @@ static void getAddressesInHeightRange( } } +// insightexplorer UniValue getaddressdeltas(const UniValue& params, bool fHelp) { std::string enableArg = "insightexplorer"; - bool fEnableGetAddressDeltas = fExperimentalMode && fInsightExplorer; + bool enabled = fExperimentalMode && fInsightExplorer; std::string disabledMsg = ""; - if (!fEnableGetAddressDeltas) { + if (!enabled) { disabledMsg = experimentalDisabledHelpMsg("getaddressdeltas", enableArg); } if (fHelp || params.size() != 1) @@ -856,7 +861,7 @@ UniValue getaddressdeltas(const UniValue& params, bool fHelp) + HelpExampleRpc("getaddressdeltas", "{\"addresses\": [\"tmYXBYJj1K7vhejSec5osXK2QsGa5MTisUQ\"], \"start\": 1000, \"end\": 2000, \"chainInfo\": true}") ); - if (!fEnableGetAddressDeltas) { + if (!enabled) { throw JSONRPCError(RPC_MISC_ERROR, "Error: getaddressdeltas is disabled. " "Run './zcash-cli help getaddressdeltas' for instructions on how to enable this feature."); } @@ -920,12 +925,13 @@ UniValue getaddressdeltas(const UniValue& params, bool fHelp) return result; } +// insightexplorer UniValue getaddressbalance(const UniValue& params, bool fHelp) { std::string enableArg = "insightexplorer"; - bool fEnableGetAddressBalance = fExperimentalMode && fInsightExplorer; + bool enabled = fExperimentalMode && fInsightExplorer; std::string disabledMsg = ""; - if (!fEnableGetAddressBalance) { + if (!enabled) { disabledMsg = experimentalDisabledHelpMsg("getaddressbalance", enableArg); } if (fHelp || params.size() != 1) @@ -953,7 +959,7 @@ UniValue getaddressbalance(const UniValue& params, bool fHelp) + HelpExampleRpc("getaddressbalance", "{\"addresses\": [\"tmYXBYJj1K7vhejSec5osXK2QsGa5MTisUQ\"]}") ); - if (!fEnableGetAddressBalance) { + if (!enabled) { throw JSONRPCError(RPC_MISC_ERROR, "Error: getaddressbalance is disabled. " "Run './zcash-cli help getaddressbalance' for instructions on how to enable this feature."); } @@ -978,12 +984,13 @@ UniValue getaddressbalance(const UniValue& params, bool fHelp) return result; } +// insightexplorer UniValue getaddresstxids(const UniValue& params, bool fHelp) { std::string enableArg = "insightexplorer"; - bool fEnableGetAddressTxids = fExperimentalMode && fInsightExplorer; + bool enabled = fExperimentalMode && fInsightExplorer; std::string disabledMsg = ""; - if (!fEnableGetAddressTxids) { + if (!enabled) { disabledMsg = experimentalDisabledHelpMsg("getaddresstxids", enableArg); } if (fHelp || params.size() != 1) @@ -1014,7 +1021,7 @@ UniValue getaddresstxids(const UniValue& params, bool fHelp) + HelpExampleRpc("getaddresstxids", "{\"addresses\": [\"tmYXBYJj1K7vhejSec5osXK2QsGa5MTisUQ\"], \"start\": 1000, \"end\": 2000}") ); - if (!fEnableGetAddressTxids) { + if (!enabled) { throw JSONRPCError(RPC_MISC_ERROR, "Error: getaddresstxids is disabled. " "Run './zcash-cli help getaddresstxids' for instructions on how to enable this feature."); } @@ -1044,6 +1051,64 @@ UniValue getaddresstxids(const UniValue& params, bool fHelp) return result; } +// insightexplorer +UniValue getspentinfo(const UniValue& params, bool fHelp) +{ + std::string enableArg = "insightexplorer"; + bool enabled = fExperimentalMode && fInsightExplorer; + std::string disabledMsg = ""; + if (!enabled) { + disabledMsg = experimentalDisabledHelpMsg("getspentinfo", enableArg); + } + if (fHelp || params.size() != 1 || !params[0].isObject()) + throw runtime_error( + "getspentinfo {\"txid\": \"txidhex\", \"index\": n}\n" + "\nReturns the txid and index where an output is spent.\n" + + disabledMsg + + "\nArguments:\n" + "{\n" + " \"txid\" (string) The hex string of the txid\n" + " \"index\" (number) The vout (output) index\n" + "}\n" + "\nResult:\n" + "{\n" + " \"txid\" (string) The transaction id\n" + " \"index\" (number) The spending (vin, input) index\n" + " ,...\n" + "}\n" + "\nExamples:\n" + + HelpExampleCli("getspentinfo", "'{\"txid\": \"0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9\", \"index\": 0}'") + + HelpExampleRpc("getspentinfo", "{\"txid\": \"0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9\", \"index\": 0}") + ); + + if (!enabled) { + throw JSONRPCError(RPC_MISC_ERROR, "Error: getspentinfo is disabled. " + "Run './zcash-cli help getspentinfo' for instructions on how to enable this feature."); + } + + UniValue txidValue = find_value(params[0].get_obj(), "txid"); + UniValue indexValue = find_value(params[0].get_obj(), "index"); + + if (!txidValue.isStr()) + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid txid, must be a string"); + if (!indexValue.isNum()) + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Invalid index, must be an integer"); + uint256 txid = ParseHashV(txidValue, "txid"); + int outputIndex = indexValue.get_int(); + + CSpentIndexKey key(txid, outputIndex); + CSpentIndexValue value; + + if (!GetSpentIndex(key, value)) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Unable to get spent info"); + } + UniValue obj(UniValue::VOBJ); + obj.push_back(Pair("txid", value.txid.GetHex())); + obj.push_back(Pair("index", (int)value.inputIndex)); + obj.push_back(Pair("height", value.blockHeight)); + + return obj; +} static const CRPCCommand commands[] = { // category name actor (function) okSafeMode @@ -1054,12 +1119,15 @@ static const CRPCCommand commands[] = { "util", "createmultisig", &createmultisig, true }, { "util", "verifymessage", &verifymessage, true }, + // START insightexplorer /* Address index */ { "addressindex", "getaddresstxids", &getaddresstxids, false }, /* insight explorer */ { "addressindex", "getaddressbalance", &getaddressbalance, false }, /* insight explorer */ { "addressindex", "getaddressdeltas", &getaddressdeltas, false }, /* insight explorer */ { "addressindex", "getaddressutxos", &getaddressutxos, false }, /* insight explorer */ { "addressindex", "getaddressmempool", &getaddressmempool, true }, /* insight explorer */ + { "blockchain", "getspentinfo", &getspentinfo, false }, /* insight explorer */ + // END insightexplorer /* Not shown in help */ { "hidden", "setmocktime", &setmocktime, true }, diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index 932c36241..441d3766c 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -197,6 +197,7 @@ void TxToJSON(const CTransaction& tx, const uint256 hashBlock, UniValue& entry) UniValue out(UniValue::VOBJ); out.push_back(Pair("value", ValueFromAmount(txout.nValue))); out.push_back(Pair("valueZat", txout.nValue)); + out.push_back(Pair("valueSat", txout.nValue)); out.push_back(Pair("n", (int64_t)i)); UniValue o(UniValue::VOBJ); ScriptPubKeyToJSON(txout.scriptPubKey, o, true); diff --git a/src/test/rpc_tests.cpp b/src/test/rpc_tests.cpp index 7ce9172e5..5b2315687 100644 --- a/src/test/rpc_tests.cpp +++ b/src/test/rpc_tests.cpp @@ -378,6 +378,12 @@ BOOST_AUTO_TEST_CASE(rpc_insightexplorer) CheckRPCThrows("getaddresstxids \"a\"", "Error: getaddresstxids is disabled. " "Run './zcash-cli help getaddresstxids' for instructions on how to enable this feature."); + CheckRPCThrows("getspentinfo {\"a\":1}", + "Error: getspentinfo is disabled. " + "Run './zcash-cli help getspentinfo' for instructions on how to enable this feature."); + CheckRPCThrows("getblockdeltas \"a\"", + "Error: getblockdeltas is disabled. " + "Run './zcash-cli help getblockdeltas' for instructions on how to enable this feature."); fExperimentalMode = true; fInsightExplorer = true; @@ -416,6 +422,17 @@ BOOST_AUTO_TEST_CASE(rpc_insightexplorer) CheckRPCThrows("getaddresstxids {\"addresses\":[],\"start\":2,\"end\":3,\"chainInfo\":true}", "Start or end is outside chain range"); + // transaction does not exist: + CheckRPCThrows("getspentinfo {\"txid\":\"b4cc287e58f87cdae59417329f710f3ecd75a4ee1d2872b7248f50977c8493f3\",\"index\":0}", + "Unable to get spent info"); + CheckRPCThrows("getspentinfo {\"txid\":\"b4cc287e58f87cdae59417329f710f3ecd75a4ee1d2872b7248f50977c8493f3\"}", + "Invalid index, must be an integer"); + CheckRPCThrows("getspentinfo {\"txid\":\"hello\",\"index\":0}", + "txid must be hexadecimal string (not 'hello')"); + + CheckRPCThrows("getblockdeltas \"00040fe8ec8471911baa1db1266ea15dd06b4a8a5c453883c000b031973dce08\"", + "Block not found"); + // revert fExperimentalMode = false; fInsightExplorer = false; diff --git a/src/txmempool.cpp b/src/txmempool.cpp index 2fc82cfb4..9c03b023d 100644 --- a/src/txmempool.cpp +++ b/src/txmempool.cpp @@ -153,6 +153,7 @@ void CTxMemPool::addAddressIndex(const CTxMemPoolEntry &entry, const CCoinsViewC mapAddressInserted.insert(make_pair(txhash, inserted)); } +// START insightexplorer void CTxMemPool::getAddressIndex( const std::vector>& addresses, std::vector>& results) @@ -181,6 +182,52 @@ void CTxMemPool::removeAddressIndex(const uint256& txhash) } } +void CTxMemPool::addSpentIndex(const CTxMemPoolEntry &entry, const CCoinsViewCache &view) +{ + LOCK(cs); + const CTransaction& tx = entry.GetTx(); + uint256 txhash = tx.GetHash(); + std::vector inserted; + + for (unsigned int j = 0; j < tx.vin.size(); j++) { + const CTxIn input = tx.vin[j]; + const CTxOut &prevout = view.GetOutputFor(input); + CSpentIndexKey key = CSpentIndexKey(input.prevout.hash, input.prevout.n); + CSpentIndexValue value = CSpentIndexValue(txhash, j, -1, prevout.nValue, + prevout.scriptPubKey.GetType(), + prevout.scriptPubKey.AddressHash()); + mapSpent.insert(make_pair(key, value)); + inserted.push_back(key); + } + mapSpentInserted.insert(make_pair(txhash, inserted)); +} + +bool CTxMemPool::getSpentIndex(const CSpentIndexKey &key, CSpentIndexValue &value) +{ + LOCK(cs); + std::map::iterator it = mapSpent.find(key); + if (it != mapSpent.end()) { + value = it->second; + return true; + } + return false; +} + +void CTxMemPool::removeSpentIndex(const uint256 txhash) +{ + LOCK(cs); + auto it = mapSpentInserted.find(txhash); + + if (it != mapSpentInserted.end()) { + std::vector keys = (*it).second; + for (std::vector::iterator mit = keys.begin(); mit != keys.end(); mit++) { + mapSpent.erase(*mit); + } + mapSpentInserted.erase(it); + } +} +// END insightexplorer + void CTxMemPool::remove(const CTransaction &origTx, std::list& removed, bool fRecursive) { // Remove transaction from memory pool @@ -231,9 +278,12 @@ void CTxMemPool::remove(const CTransaction &origTx, std::list& rem mapTx.erase(hash); nTransactionsUpdated++; minerPolicyEstimator->removeTx(hash); + // insightexplorer if (fAddressIndex) removeAddressIndex(hash); + if (fSpentIndex) + removeSpentIndex(hash); } } } diff --git a/src/txmempool.h b/src/txmempool.h index d3689c64f..6cd87a79c 100644 --- a/src/txmempool.h +++ b/src/txmempool.h @@ -13,6 +13,7 @@ #include "primitives/transaction.h" #include "sync.h" #include "addressindex.h" +#include "spentindex.h" #undef foreach #include "boost/multi_index_container.hpp" @@ -155,8 +156,11 @@ public: indexed_transaction_set mapTx; private: + // insightexplorer std::map mapAddress; std::map > mapAddressInserted; + std::map mapSpent; + std::map> mapSpentInserted; public: std::map mapNextTx; @@ -176,11 +180,17 @@ public: bool addUnchecked(const uint256& hash, const CTxMemPoolEntry &entry, bool fCurrentEstimate = true); + // START insightexplorer void addAddressIndex(const CTxMemPoolEntry &entry, const CCoinsViewCache &view); void getAddressIndex(const std::vector>& addresses, std::vector>& results); void removeAddressIndex(const uint256& txhash); + void addSpentIndex(const CTxMemPoolEntry &entry, const CCoinsViewCache &view); + bool getSpentIndex(const CSpentIndexKey &key, CSpentIndexValue &value); + void removeSpentIndex(const uint256 txhash); + // END insightexplorer + void remove(const CTransaction &tx, std::list& removed, bool fRecursive = false); void removeWithAnchor(const uint256 &invalidRoot, ShieldedType type); void removeForReorg(const CCoinsViewCache *pcoins, unsigned int nMemPoolHeight, int flags);