diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index c17ca2fa3a1..2e2e8d20c87 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -46,6 +46,33 @@ static CUpdatedBlock latestblock; extern void TxToJSON(const CTransaction& tx, const uint256 hashBlock, UniValue& entry); +static bool ReadTxoFromOutpoint(Coin& coin, bool& is_spent, const CBlockIndex* pindex, const COutPoint& out, bool include_spent) +{ + is_spent = false; + if (pcoinsTip->GetCoin(out, coin)) { + return true; + } else if (!include_spent) { + return false; + } + is_spent = true; + + uint256 hashblock; + CTransactionRef tx_ref; + if (!GetBlockchainTx(out.hash, tx_ref, Params().GetConsensus(), hashblock, true)) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, std::string(fTxIndex ? "No such blockchain transaction" + : "No such utxo blockchain transaction. Use -txindex to enable spent output queries")); + } + if (out.n >= tx_ref->vout.size()) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, std::string(strprintf("No output %d for tx %s", out.n, out.hash.GetHex()))); + } + const CTxOut& prevoutput = tx_ref->vout[out.n]; + coin.out = prevoutput; + coin.fCoinBase = tx_ref->IsCoinBase(); + coin.nHeight = mapBlockIndex[hashblock]->nHeight; + + return true; +} + double GetDifficulty(const CBlockIndex* blockindex) { if (blockindex == NULL) @@ -939,14 +966,15 @@ UniValue gettxoutsetinfo(const JSONRPCRequest& request) UniValue gettxout(const JSONRPCRequest& request) { - if (request.fHelp || request.params.size() < 2 || request.params.size() > 3) + if (request.fHelp || request.params.size() < 2 || request.params.size() > 4) throw std::runtime_error( "gettxout \"txid\" n ( include_mempool )\n" - "\nReturns details about an unspent transaction output.\n" + "\nReturns details about a transaction output.\n" "\nArguments:\n" - "1. \"txid\" (string, required) The transaction id\n" - "2. n (numeric, required) vout number\n" - "3. include_mempool (boolean, optional) Whether to include the mempool\n" + "1. \"txid\" (string, required) The transaction id\n" + "2. \"n\" (numeric, required) vout number\n" + "3. \"include_mempool\" (boolean, optional) Only search in the mempool. Default: true\n" + "4. \"include_spent\" (boolean, optional) Whether to include spent outputs. Default: false\n" "\nResult:\n" "{\n" " \"bestblock\" : \"hash\", (string) the block hash\n" @@ -964,6 +992,7 @@ UniValue gettxout(const JSONRPCRequest& request) " },\n" " \"version\" : n, (numeric) The version\n" " \"coinbase\" : true|false (boolean) Coinbase or not\n" + " \"spent\" : true|false (boolean) Is the output spent with the current tip or not\n" "}\n" "\nExamples:\n" @@ -987,6 +1016,12 @@ UniValue gettxout(const JSONRPCRequest& request) if (request.params.size() > 2) fMempool = request.params[2].get_bool(); + bool include_spent = false; + if (request.params.size() > 3) { + include_spent = request.params[3].get_bool(); + } + + bool is_spent = false; Coin coin; if (fMempool) { LOCK(mempool.cs); @@ -995,7 +1030,7 @@ UniValue gettxout(const JSONRPCRequest& request) return NullUniValue; } } else { - if (!pcoinsTip->GetCoin(out, coin)) { + if (!ReadTxoFromOutpoint(coin, is_spent, chainActive.Tip(), out, include_spent)) { return NullUniValue; } } @@ -1013,6 +1048,7 @@ UniValue gettxout(const JSONRPCRequest& request) ScriptPubKeyToUniv(coin.out.scriptPubKey, o, true); ret.push_back(Pair("scriptPubKey", o)); ret.push_back(Pair("coinbase", (bool)coin.fCoinBase)); + ret.push_back(Pair("spent", (bool)is_spent)); return ret; } diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index a3ea5390eec..d0da590085b 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -93,6 +93,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "fundrawtransaction", 1, "options" }, { "gettxout", 1, "n" }, { "gettxout", 2, "include_mempool" }, + { "gettxout", 3, "include_spent" }, { "gettxoutproof", 0, "txids" }, { "lockunspent", 0, "unlock" }, { "lockunspent", 1, "transactions" }, diff --git a/src/validation.cpp b/src/validation.cpp index 09288be1ca4..58d8a871ff7 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -900,19 +900,12 @@ bool AcceptToMemoryPool(CTxMemPool& pool, CValidationState &state, const CTransa } /** Return transaction in txOut, and if it was found inside a block, its hash is placed in hashBlock */ -bool GetTransaction(const uint256 &hash, CTransactionRef &txOut, const Consensus::Params& consensusParams, uint256 &hashBlock, bool fAllowSlow) +bool GetBlockchainTx(const uint256 &hash, CTransactionRef &txOut, const Consensus::Params& consensusParams, uint256 &hashBlock, bool fAllowSlow) { CBlockIndex *pindexSlow = NULL; LOCK(cs_main); - CTransactionRef ptx = mempool.get(hash); - if (ptx) - { - txOut = ptx; - return true; - } - if (fTxIndex) { CDiskTxPos postx; if (pblocktree->ReadTxIndex(hash, postx)) { @@ -955,6 +948,23 @@ bool GetTransaction(const uint256 &hash, CTransactionRef &txOut, const Consensus return false; } +/** + * Return transaction in txOut, and if it was found inside a block, + * its hash is placed in hashBlock. Look in the mempool first. +*/ +bool GetTransaction(const uint256 &hash, CTransactionRef &txOut, const Consensus::Params& consensusParams, uint256 &hashBlock, bool fAllowSlow) +{ + LOCK(cs_main); + + CTransactionRef ptx = mempool.get(hash); + if (ptx) + { + txOut = ptx; + return true; + } + return GetBlockchainTx(hash, txOut, consensusParams, hashBlock, fAllowSlow); +} + diff --git a/src/validation.h b/src/validation.h index a9f995abb88..baba5cda380 100644 --- a/src/validation.h +++ b/src/validation.h @@ -276,6 +276,8 @@ bool IsInitialBlockDownload(); * This function only returns the highest priority warning of the set selected by strFor. */ std::string GetWarnings(const std::string& strFor); +/** Retrieve a transaction (from disk (not mempool), if possible) */ +bool GetBlockchainTx(const uint256 &hash, CTransactionRef &txOut, const Consensus::Params& consensusParams, uint256 &hashBlock, bool fAllowSlow); /** Retrieve a transaction (from memory pool, or from disk, if possible) */ bool GetTransaction(const uint256 &hash, CTransactionRef &tx, const Consensus::Params& params, uint256 &hashBlock, bool fAllowSlow = false); /** Find the best known block, and make it the tip of the block chain */ diff --git a/test/functional/wallet.py b/test/functional/wallet.py index 3e3e8fcddb3..3a20abe80c5 100755 --- a/test/functional/wallet.py +++ b/test/functional/wallet.py @@ -19,6 +19,7 @@ def __init__(self): self.setup_clean_chain = True self.num_nodes = 4 self.extra_args = [['-usehd={:d}'.format(i%2==0)] for i in range(4)] + self.extra_args[0].append('-txindex') def setup_network(self): self.nodes = self.start_nodes(3, self.options.tmpdir, self.extra_args[:3]) @@ -71,6 +72,8 @@ def run_test(self): confirmed_txid, confirmed_index = utxos[0]["txid"], utxos[0]["vout"] txout = self.nodes[0].gettxout(confirmed_txid, confirmed_index, False) assert_equal(txout['value'], 50) + assert_equal(txout['confirmations'], 102) + assert_equal(txout['spent'], False) txout = self.nodes[0].gettxout(confirmed_txid, confirmed_index, True) assert txout is None # new utxo from mempool should be invisible if you exclude mempool @@ -83,13 +86,29 @@ def run_test(self): # but 10 will go to node2 and the rest will go to node0 balance = self.nodes[0].getbalance() assert_equal(set([txout1['value'], txout2['value']]), set([10, balance])) + assert_equal(txout1['spent'], False) walletinfo = self.nodes[0].getwalletinfo() assert_equal(walletinfo['immature_balance'], 0) + assert_raises_jsonrpc(-5, 'No such blockchain transaction', self.nodes[0].gettxout, mempool_txid, 0, False, True) # Have node0 mine a block, thus it will collect its own fee. self.nodes[0].generate(1) self.sync_all() + # Now the txo should be visible only if you include spent outputs + txout = self.nodes[0].gettxout(confirmed_txid, confirmed_index, False, False) + assert txout is None + txout = self.nodes[0].gettxout(confirmed_txid, confirmed_index, True, False) + assert txout is None + txout = self.nodes[0].gettxout(confirmed_txid, confirmed_index, True, True) + assert txout is None + txout = self.nodes[0].gettxout(confirmed_txid, confirmed_index, False, True) + assert_equal(txout['value'], 50) + assert_equal(txout['confirmations'], 103) + assert_equal(txout['spent'], True) + assert_raises_jsonrpc(-5, 'No output %d for tx %s' % (100, confirmed_txid), self.nodes[0].gettxout, confirmed_txid, 100, False, True) + assert_raises_jsonrpc(-5, 'No such utxo blockchain transaction. Use -txindex to enable spent output queries', self.nodes[1].gettxout, confirmed_txid, confirmed_index, False, True) + # Exercise locking of unspent outputs unspent_0 = self.nodes[2].listunspent()[0] unspent_0 = {"txid": unspent_0["txid"], "vout": unspent_0["vout"]}