diff --git a/doc/release-notes.md b/doc/release-notes.md --- a/doc/release-notes.md +++ b/doc/release-notes.md @@ -3,3 +3,6 @@ This release includes the following features and fixes: + - Added optional `blockhash` parameter to `getrawtransaction` to narrowly + search for a transaction within a given block. New returned field + `in_active_chain` will indicate if that block is part of the active chain. diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -65,13 +65,20 @@ static UniValue getrawtransaction(const Config &config, const JSONRPCRequest &request) { if (request.fHelp || request.params.size() < 1 || - request.params.size() > 2) { + request.params.size() > 3) { throw std::runtime_error( - "getrawtransaction \"txid\" ( verbose )\n" + "getrawtransaction \"txid\" ( verbose \"blockhash\" )\n" "\nNOTE: By default this function only works for mempool " "transactions. If the -txindex option is\n" - "enabled, it also works for blockchain transactions.\n" + "enabled, it also works for blockchain transactions. If the block " + "which contains the transaction\n" + "is known, its hash can be provided even for nodes without " + "-txindex. Note that if a blockhash is\n" + "provided, only that block will be searched and if the transaction " + "is in the mempool or other\n" + "blocks, or if this node does not have the given block available, " + "the transaction will not be found.\n" "DEPRECATED: for now, it also works for transactions with unspent " "outputs.\n" @@ -83,8 +90,10 @@ "\nArguments:\n" "1. \"txid\" (string, required) The transaction id\n" - "2. verbose (bool, optional, default=false) If false, return " - "a string, otherwise return a json object\n" + "2. verbose (bool, optional, default=false) If false, return a " + "string, otherwise return a json object\n" + "3. \"blockhash\" (string, optional) The block in which to look " + "for the transaction\n" "\nResult (if verbose is not set or set to false):\n" "\"data\" (string) The serialized, hex-encoded data for " @@ -92,6 +101,9 @@ "\nResult (if verbose is set to true):\n" "{\n" + " \"in_active_chain\": b, (bool) Whether specified block is in " + "the active chain or not (only present with explicit \"blockhash\" " + "argument)\n" " \"hex\" : \"data\", (string) The serialized, hex-encoded " "data for 'txid'\n" " \"txid\" : \"id\", (string) The transaction id (same as " @@ -145,40 +157,57 @@ "\nExamples:\n" + HelpExampleCli("getrawtransaction", "\"mytxid\"") + HelpExampleCli("getrawtransaction", "\"mytxid\" true") + - HelpExampleRpc("getrawtransaction", "\"mytxid\", true")); + HelpExampleRpc("getrawtransaction", "\"mytxid\", true") + + HelpExampleCli("getrawtransaction", + "\"mytxid\" false \"myblockhash\"") + + HelpExampleCli("getrawtransaction", + "\"mytxid\" true \"myblockhash\"")); } LOCK(cs_main); + bool in_active_chain = true; TxId txid = TxId(ParseHashV(request.params[0], "parameter 1")); + CBlockIndex *blockindex = nullptr; // Accept either a bool (true) or a num (>=1) to indicate verbose output. bool fVerbose = false; if (!request.params[1].isNull()) { - if (request.params[1].isNum()) { - if (request.params[1].get_int() != 0) { - fVerbose = true; - } - } else if (request.params[1].isBool()) { - if (request.params[1].isTrue()) { - fVerbose = true; + fVerbose = request.params[1].isNum() + ? (request.params[1].get_int() != 0) + : request.params[1].get_bool(); + } + + if (!request.params[2].isNull()) { + uint256 blockhash = ParseHashV(request.params[2], "parameter 3"); + if (!blockhash.IsNull()) { + BlockMap::iterator it = mapBlockIndex.find(blockhash); + if (it == mapBlockIndex.end()) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, + "Block hash not found"); } - } else { - throw JSONRPCError( - RPC_TYPE_ERROR, - "Invalid type provided. Verbose parameter must be a boolean."); + blockindex = it->second; + in_active_chain = chainActive.Contains(blockindex); } } CTransactionRef tx; - uint256 hashBlock; - if (!GetTransaction(config, txid, tx, hashBlock, true)) { - throw JSONRPCError( - RPC_INVALID_ADDRESS_OR_KEY, - std::string(fTxIndex ? "No such mempool or blockchain transaction" - : "No such mempool transaction. Use -txindex " - "to enable blockchain transaction queries") + - ". Use gettransaction for wallet transactions."); + uint256 hash_block; + if (!GetTransaction(config, txid, tx, hash_block, true, blockindex)) { + std::string errmsg; + if (blockindex) { + if (!blockindex->nStatus.hasData()) { + throw JSONRPCError(RPC_MISC_ERROR, "Block not available"); + } + errmsg = "No such transaction found in the provided block"; + } else { + errmsg = fTxIndex ? "No such mempool or blockchain transaction" + : "No such mempool transaction. Use -txindex to " + "enable blockchain transaction queries"; + } + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, + errmsg + + ". Use gettransaction for wallet transactions."); } if (!fVerbose) { @@ -186,7 +215,10 @@ } UniValue result(UniValue::VOBJ); - TxToJSON(*tx, hashBlock, result); + if (blockindex) { + result.pushKV("in_active_chain", in_active_chain); + } + TxToJSON(*tx, hash_block, result); return result; } @@ -1293,7 +1325,7 @@ static const ContextFreeRPCCommand commands[] = { // category name actor (function) argNames // ------------------- ------------------------ ---------------------- ---------- - { "rawtransactions", "getrawtransaction", getrawtransaction, {"txid","verbose"} }, + { "rawtransactions", "getrawtransaction", getrawtransaction, {"txid","verbose","blockhash"} }, { "rawtransactions", "createrawtransaction", createrawtransaction, {"inputs","outputs","locktime"} }, { "rawtransactions", "decoderawtransaction", decoderawtransaction, {"hexstring"} }, { "rawtransactions", "decodescript", decodescript, {"hexstring"} }, diff --git a/src/validation.h b/src/validation.h --- a/src/validation.h +++ b/src/validation.h @@ -401,7 +401,8 @@ * Retrieve a transaction (from memory pool, or from disk, if possible). */ bool GetTransaction(const Config &config, const TxId &txid, CTransactionRef &tx, - uint256 &hashBlock, bool fAllowSlow = false); + uint256 &hashBlock, bool fAllowSlow = false, + CBlockIndex *blockIndex = nullptr); /** * Find the best known block, and make it the active tip of the block chain. diff --git a/src/validation.cpp b/src/validation.cpp --- a/src/validation.cpp +++ b/src/validation.cpp @@ -771,57 +771,58 @@ /** * Return transaction in txOut, and if it was found inside a block, its hash is - * placed in hashBlock. + * placed in hashBlock. If blockIndex is provided, the transaction is fetched + * from the corresponding block. */ bool GetTransaction(const Config &config, const TxId &txid, - CTransactionRef &txOut, uint256 &hashBlock, - bool fAllowSlow) { - CBlockIndex *pindexSlow = nullptr; + CTransactionRef &txOut, uint256 &hashBlock, bool fAllowSlow, + CBlockIndex *blockIndex) { + CBlockIndex *pindexSlow = blockIndex; LOCK(cs_main); - CTransactionRef ptx = g_mempool.get(txid); - if (ptx) { - txOut = ptx; - return true; - } - - if (fTxIndex) { - CDiskTxPos postx; - if (pblocktree->ReadTxIndex(txid, postx)) { - CAutoFile file(OpenBlockFile(postx, true), SER_DISK, - CLIENT_VERSION); - if (file.IsNull()) { - return error("%s: OpenBlockFile failed", __func__); - } - - CBlockHeader header; - try { - file >> header; - fseek(file.Get(), postx.nTxOffset, SEEK_CUR); - file >> txOut; - } catch (const std::exception &e) { - return error("%s: Deserialize or I/O error - %s", __func__, - e.what()); - } + if (!blockIndex) { + CTransactionRef ptx = g_mempool.get(txid); + if (ptx) { + txOut = ptx; + return true; + } - hashBlock = header.GetHash(); - if (txOut->GetId() != txid) { - return error("%s: txid mismatch", __func__); + if (fTxIndex) { + CDiskTxPos postx; + if (pblocktree->ReadTxIndex(txid, postx)) { + CAutoFile file(OpenBlockFile(postx, true), SER_DISK, + CLIENT_VERSION); + if (file.IsNull()) { + return error("%s: OpenBlockFile failed", __func__); + } + CBlockHeader header; + try { + file >> header; + fseek(file.Get(), postx.nTxOffset, SEEK_CUR); + file >> txOut; + } catch (const std::exception &e) { + return error("%s: Deserialize or I/O error - %s", __func__, + e.what()); + } + hashBlock = header.GetHash(); + if (txOut->GetId() != txid) { + return error("%s: txid mismatch", __func__); + } + return true; } - return true; + // transaction not found in index, nothing more can be done + return false; } - // transaction not found in index, nothing more can be done - return false; - } - - // use coin database to locate block that contains transaction, and scan it - if (fAllowSlow) { - const Coin &coin = AccessByTxid(*pcoinsTip, txid); - if (!coin.IsSpent()) { - pindexSlow = chainActive[coin.GetHeight()]; + // use coin database to locate block that contains transaction, and scan + // it + if (fAllowSlow) { + const Coin &coin = AccessByTxid(*pcoinsTip, txid); + if (!coin.IsSpent()) { + pindexSlow = chainActive[coin.GetHeight()]; + } } } diff --git a/test/functional/rpc_rawtransaction.py b/test/functional/rpc_rawtransaction.py --- a/test/functional/rpc_rawtransaction.py +++ b/test/functional/rpc_rawtransaction.py @@ -62,6 +62,35 @@ assert_raises_rpc_error( -25, "Missing inputs", self.nodes[2].sendrawtransaction, rawtx['hex']) + ##################################### + # getrawtransaction with block hash # + ##################################### + + # make a tx by sending then generate 2 blocks; block1 has the tx in it + tx = self.nodes[2].sendtoaddress(self.nodes[1].getnewaddress(), 1) + block1, block2 = self.nodes[2].generate(2) + self.sync_all() + # We should be able to get the raw transaction by providing the correct block + gottx = self.nodes[0].getrawtransaction(tx, True, block1) + assert_equal(gottx['txid'], tx) + assert_equal(gottx['in_active_chain'], True) + # We should not have the 'in_active_chain' flag when we don't provide a block + gottx = self.nodes[0].getrawtransaction(tx, True) + assert_equal(gottx['txid'], tx) + assert 'in_active_chain' not in gottx + # We should not get the tx if we provide an unrelated block + assert_raises_rpc_error(-5, "No such transaction found", + self.nodes[0].getrawtransaction, tx, True, block2) + # An invalid block hash should raise the correct errors + assert_raises_rpc_error(-8, "parameter 3 must be hexadecimal", + self.nodes[0].getrawtransaction, tx, True, True) + assert_raises_rpc_error(-8, "parameter 3 must be hexadecimal", + self.nodes[0].getrawtransaction, tx, True, "foobar") + assert_raises_rpc_error(-8, "parameter 3 must be of length 64", + self.nodes[0].getrawtransaction, tx, True, "abcd1234") + assert_raises_rpc_error(-5, "Block hash not found", self.nodes[0].getrawtransaction, + tx, True, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + # # RAW TX MULTISIG TESTS # # @@ -235,15 +264,15 @@ # 6. invalid parameters - supply txid and string "Flase" assert_raises_rpc_error( - -3, "Invalid type", self.nodes[0].getrawtransaction, txHash, "False") + -1, "not a boolean", self.nodes[0].getrawtransaction, txHash, "False") # 7. invalid parameters - supply txid and empty array assert_raises_rpc_error( - -3, "Invalid type", self.nodes[0].getrawtransaction, txHash, []) + -1, "not a boolean", self.nodes[0].getrawtransaction, txHash, []) # 8. invalid parameters - supply txid and empty dict assert_raises_rpc_error( - -3, "Invalid type", self.nodes[0].getrawtransaction, txHash, {}) + -1, "not a boolean", self.nodes[0].getrawtransaction, txHash, {}) # Sanity checks on verbose getrawtransaction output rawTxOutput = self.nodes[0].getrawtransaction(txHash, True)