diff options
author | Andrew Chow <github@achow101.com> | 2022-12-13 17:59:26 -0500 |
---|---|---|
committer | Andrew Chow <github@achow101.com> | 2022-12-13 18:09:09 -0500 |
commit | daf881de9db195f0ce5b8067edba2eef7a962abf (patch) | |
tree | b613786a30fbef6b1471ee81b51a2c6483c6587f | |
parent | ffa32ab108a4bfb53f012549bc4e3c224a6aca3e (diff) | |
parent | f86697163e5cdbc3bc4a65cfb7dbaa3d9eb602a9 (diff) | |
download | bitcoin-daf881de9db195f0ce5b8067edba2eef7a962abf.tar.xz |
Merge bitcoin/bitcoin#23319: rpc: Return fee and prevout (utxos) to getrawtransaction
f86697163e5cdbc3bc4a65cfb7dbaa3d9eb602a9 rpc: Return fee and prevout(s) to getrawtransaction (Douglas Chimento)
Pull request description:
Add fee response in BTC to getrawtransaction #23264
### For Reviewers
* Verbose arg is now an int
* Verbose = 2 includes a `fee` field and `prevout`
* [./test/functional/rpc_rawtransaction.py](./test/functional/rpc_rawtransaction.py) contains a new test to validate fields of new verbosity 2 (not the values)
```
bitcoin-cli -chain=test getrawtransaction 9ae533f7da9be4a34997db78343a8d8d6d6186b6bba3959e56f416a5c70e7de4 2 000000000000001d442e556146d5f2841d85150c200e8d8b8a4b5005b13878f6
```
```
"in_active_chain": true,
"txid": "9ae533f7da9be4a34997db78343a8d8d6d6186b6bba3959e56f416a5c70e7de4",
"hash": "7f23e3f3a0a256ddea1d35ffd43e9afdd67cc68389ef1a804bb20c76abd6863e",
....
"vin": [
{
"txid": "23fc75d6d74f6f97e225839af69ff36a612fe04db58a4414ec4828d1749a05a0",
"vout": 0,
"scriptSig": {
"asm": "",
"hex": ""
},
"prevout": {
"generated": false,
"height": 2099486,
"value": 0.00017764,
"scriptPubKey": {
"asm": "0 7846ce1ced3253d8bd43008db2ca364cc722f5a2",
"hex": "00147846ce1ced3253d8bd43008db2ca364cc722f5a2",
"address": "tb1q0prvu88dxffa302rqzxm9j3kfnrj9adzk49mlp",
"type": "witness_v0_keyhash"
}
},
"sequence": 4294967295
},
...
"fee": 0.00000762
}
```
ACKs for top commit:
achow101:
ACK f86697163e5cdbc3bc4a65cfb7dbaa3d9eb602a9
aureleoules:
ACK f86697163e5cdbc3bc4a65cfb7dbaa3d9eb602a9
hernanmarino:
re ACK f86697163e5cdbc3bc4a65cfb7dbaa3d9eb602a9
pablomartin4btc:
re-tACK f86697163e5cdbc3bc4a65cfb7dbaa3d9eb602a9
Tree-SHA512: 591fdc285d74fa7803e04ad01c7b70bc20fac6b1369e7bd5b8e2cde9b750ea52d6c70d79225b74bef4f4bbc0fb960877778017184e146119da4a55f9593d1224
-rw-r--r-- | src/rpc/client.cpp | 1 | ||||
-rw-r--r-- | src/rpc/rawtransaction.cpp | 112 | ||||
-rwxr-xr-x | test/functional/rpc_rawtransaction.py | 81 |
3 files changed, 164 insertions, 30 deletions
diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index ea094976bf..a795296f35 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -102,6 +102,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "getchaintxstats", 0, "nblocks" }, { "gettransaction", 1, "include_watchonly" }, { "gettransaction", 2, "verbose" }, + { "getrawtransaction", 1, "verbosity" }, { "getrawtransaction", 1, "verbose" }, { "createrawtransaction", 0, "inputs" }, { "createrawtransaction", 1, "outputs" }, diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index 400f2f1507..df30eaaa97 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -32,6 +32,7 @@ #include <script/signingprovider.h> #include <script/standard.h> #include <uint256.h> +#include <undo.h> #include <util/bip32.h> #include <util/check.h> #include <util/strencodings.h> @@ -50,15 +51,17 @@ using node::FindCoins; using node::GetTransaction; using node::NodeContext; using node::PSBTAnalysis; +using node::ReadBlockFromDisk; +using node::UndoReadFromDisk; -static void TxToJSON(const CTransaction& tx, const uint256 hashBlock, UniValue& entry, Chainstate& active_chainstate) +static void TxToJSON(const CTransaction& tx, const uint256 hashBlock, UniValue& entry, Chainstate& active_chainstate, const CTxUndo* txundo = nullptr, TxVerbosity verbosity = TxVerbosity::SHOW_TXID) { // Call into TxToUniv() in bitcoin-common to decode the transaction hex. // // Blockchain contextual information (confirmations and blocktime) is not // available to code in bitcoin-common, so we query them here and push the // data into the returned UniValue. - TxToUniv(tx, /*block_hash=*/uint256(), entry, /*include_hex=*/true, RPCSerializationFlags()); + TxToUniv(tx, /*block_hash=*/uint256(), entry, /*include_hex=*/true, RPCSerializationFlags(), txundo, verbosity); if (!hashBlock.IsNull()) { LOCK(cs_main); @@ -166,26 +169,27 @@ static RPCHelpMan getrawtransaction() { return RPCHelpMan{ "getrawtransaction", - "Return the raw transaction data.\n" - "\nBy default, this call only returns a transaction if it is in the mempool. If -txindex is enabled\n" + "By default, this call only returns a transaction if it is in the mempool. If -txindex is enabled\n" "and no blockhash argument is passed, it will return the transaction if it is in the mempool or any block.\n" "If a blockhash argument is passed, it will return the transaction if\n" - "the specified block is available and the transaction is in that block.\n" - "\nHint: Use gettransaction for wallet transactions.\n" + "the specified block is available and the transaction is in that block.\n\n" + "Hint: Use gettransaction for wallet transactions.\n\n" - "\nIf verbose is 'true', returns an Object with information about 'txid'.\n" - "If verbose is 'false' or omitted, returns a string that is serialized, hex-encoded data for 'txid'.", + "If verbosity is 0 or omitted, returns the serialized transaction as a hex-encoded string.\n" + "If verbosity is 1, returns a JSON Object with information about transaction.\n" + "If verbosity is 2, returns a JSON Object with information about transaction, including fee and prevout information.", { {"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The transaction id"}, - {"verbose", RPCArg::Type::BOOL, RPCArg::Default{false}, "If false, return a string, otherwise return a json object"}, + {"verbosity|verbose", RPCArg::Type::NUM, RPCArg::Default{0}, "0 for hex-encoded data, 1 for a JSON object, and 2 for JSON object with fee and prevout"}, {"blockhash", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED_NAMED_ARG, "The block in which to look for the transaction"}, }, { - RPCResult{"if verbose is not set or set to false", - RPCResult::Type::STR, "data", "The serialized, hex-encoded data for 'txid'" + RPCResult{"if verbosity is not set or set to 0", + RPCResult::Type::STR, "data", "The serialized transaction as a hex-encoded string for 'txid'" }, - RPCResult{"if verbose is set to true", + RPCResult{"if verbosity is set to 1", + // When updating this documentation, update `decoderawtransaction` in the same way. RPCResult::Type::OBJ, "", "", Cat<std::vector<RPCResult>>( { @@ -198,20 +202,47 @@ static RPCHelpMan getrawtransaction() }, DecodeTxDoc(/*txid_field_doc=*/"The transaction id (same as provided)")), }, + RPCResult{"for verbosity = 2", + RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::ELISION, "", "Same output as verbosity = 1"}, + {RPCResult::Type::NUM, "fee", /* optional */ true, "transaction fee in " + CURRENCY_UNIT + ", omitted if block undo data is not available"}, + {RPCResult::Type::ARR, "vin", "", + { + {RPCResult::Type::OBJ, "", /* optional */ true, "utxo being spent, omitted if block undo data is not available", + { + {RPCResult::Type::ELISION, "", "Same output as verbosity = 1"}, + {RPCResult::Type::OBJ, "prevout", "Only if undo information is available)", + { + {RPCResult::Type::BOOL, "generated", "Coinbase or not"}, + {RPCResult::Type::NUM, "height", "The height of the prevout"}, + {RPCResult::Type::STR_AMOUNT, "value", "The value in " + CURRENCY_UNIT}, + {RPCResult::Type::OBJ, "scriptPubKey", "", + { + {RPCResult::Type::STR, "asm", "Disassembly of the public key script"}, + {RPCResult::Type::STR, "desc", "Inferred descriptor for the output"}, + {RPCResult::Type::STR_HEX, "hex", "The raw public key script bytes, hex-encoded"}, + {RPCResult::Type::STR, "address", /*optional=*/true, "The Bitcoin address (only if a well-defined address exists)"}, + {RPCResult::Type::STR, "type", "The type (one of: " + GetAllOutputTypes() + ")"}, + }}, + }}, + }}, + }}, + }}, }, RPCExamples{ HelpExampleCli("getrawtransaction", "\"mytxid\"") - + HelpExampleCli("getrawtransaction", "\"mytxid\" true") - + HelpExampleRpc("getrawtransaction", "\"mytxid\", true") - + HelpExampleCli("getrawtransaction", "\"mytxid\" false \"myblockhash\"") - + HelpExampleCli("getrawtransaction", "\"mytxid\" true \"myblockhash\"") + + HelpExampleCli("getrawtransaction", "\"mytxid\" 1") + + HelpExampleRpc("getrawtransaction", "\"mytxid\", 1") + + HelpExampleCli("getrawtransaction", "\"mytxid\" 0 \"myblockhash\"") + + HelpExampleCli("getrawtransaction", "\"mytxid\" 1 \"myblockhash\"") + + HelpExampleCli("getrawtransaction", "\"mytxid\" 2 \"myblockhash\"") }, [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue { const NodeContext& node = EnsureAnyNodeContext(request.context); ChainstateManager& chainman = EnsureChainman(node); - bool in_active_chain = true; uint256 hash = ParseHashV(request.params[0], "parameter 1"); const CBlockIndex* blockindex = nullptr; @@ -220,10 +251,14 @@ static RPCHelpMan getrawtransaction() throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "The genesis block coinbase is not considered an ordinary transaction and cannot be retrieved"); } - // Accept either a bool (true) or a num (>=1) to indicate verbose output. - bool fVerbose = false; + // Accept either a bool (true) or a num (>=0) to indicate verbosity. + int verbosity{0}; if (!request.params[1].isNull()) { - fVerbose = request.params[1].isNum() ? (request.params[1].getInt<int>() != 0) : request.params[1].get_bool(); + if (request.params[1].isBool()) { + verbosity = request.params[1].get_bool(); + } else { + verbosity = request.params[1].getInt<int>(); + } } if (!request.params[2].isNull()) { @@ -234,7 +269,6 @@ static RPCHelpMan getrawtransaction() if (!blockindex) { throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block hash not found"); } - in_active_chain = chainman.ActiveChain().Contains(blockindex); } bool f_txindex_ready = false; @@ -262,13 +296,43 @@ static RPCHelpMan getrawtransaction() throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, errmsg + ". Use gettransaction for wallet transactions."); } - if (!fVerbose) { + if (verbosity <= 0) { return EncodeHexTx(*tx, RPCSerializationFlags()); } UniValue result(UniValue::VOBJ); - if (blockindex) result.pushKV("in_active_chain", in_active_chain); - TxToJSON(*tx, hash_block, result, chainman.ActiveChainstate()); + if (blockindex) { + LOCK(cs_main); + result.pushKV("in_active_chain", chainman.ActiveChain().Contains(blockindex)); + } + // If request is verbosity >= 1 but no blockhash was given, then look up the blockindex + if (request.params[2].isNull()) { + LOCK(cs_main); + blockindex = chainman.m_blockman.LookupBlockIndex(hash_block); + } + if (verbosity == 1) { + TxToJSON(*tx, hash_block, result, chainman.ActiveChainstate()); + return result; + } + + CBlockUndo blockUndo; + CBlock block; + const bool is_block_pruned{WITH_LOCK(cs_main, return chainman.m_blockman.IsBlockPruned(blockindex))}; + + if (tx->IsCoinBase() || + !blockindex || is_block_pruned || + !(UndoReadFromDisk(blockUndo, blockindex) && ReadBlockFromDisk(block, blockindex, Params().GetConsensus()))) { + TxToJSON(*tx, hash_block, result, chainman.ActiveChainstate()); + return result; + } + + CTxUndo* undoTX {nullptr}; + auto it = std::find_if(block.vtx.begin(), block.vtx.end(), [tx](CTransactionRef t){ return *t == *tx; }); + if (it != block.vtx.end()) { + // -1 as blockundo does not have coinbase tx + undoTX = &blockUndo.vtxundo.at(it - block.vtx.begin() - 1); + } + TxToJSON(*tx, hash_block, result, chainman.ActiveChainstate(), undoTX, TxVerbosity::SHOW_DETAILS_AND_PREVOUT); return result; }, }; diff --git a/test/functional/rpc_rawtransaction.py b/test/functional/rpc_rawtransaction.py index 15fc947eef..b87f3ad6f3 100755 --- a/test/functional/rpc_rawtransaction.py +++ b/test/functional/rpc_rawtransaction.py @@ -14,6 +14,7 @@ Test the following RPCs: from collections import OrderedDict from decimal import Decimal +from itertools import product from test_framework.blocktools import COINBASE_MATURITY from test_framework.messages import ( @@ -81,6 +82,7 @@ class RawTransactionsTest(BitcoinTestFramework): self.generate(self.nodes[0], COINBASE_MATURITY + 1) self.getrawtransaction_tests() + self.getrawtransaction_verbosity_tests() self.createrawtransaction_tests() self.sendrawtransaction_tests() self.sendrawtransaction_testmempoolaccept_tests() @@ -116,6 +118,7 @@ class RawTransactionsTest(BitcoinTestFramework): # 4. valid parameters - supply txid and 1 for verbose. # We only check the "hex" field of the output so we don't need to update this test every time the output format changes. assert_equal(self.nodes[n].getrawtransaction(txId, 1)["hex"], tx['hex']) + assert_equal(self.nodes[n].getrawtransaction(txId, 2)["hex"], tx['hex']) # 5. valid parameters - supply txid and True for non-verbose assert_equal(self.nodes[n].getrawtransaction(txId, True)["hex"], tx['hex']) @@ -126,13 +129,14 @@ class RawTransactionsTest(BitcoinTestFramework): # 6. invalid parameters - supply txid and invalid boolean values (strings) for verbose for value in ["True", "False"]: - assert_raises_rpc_error(-3, "not of expected type bool", self.nodes[n].getrawtransaction, txid=txId, verbose=value) + assert_raises_rpc_error(-3, "not of expected type number", self.nodes[n].getrawtransaction, txid=txId, verbose=value) + assert_raises_rpc_error(-3, "not of expected type number", self.nodes[n].getrawtransaction, txid=txId, verbosity=value) # 7. invalid parameters - supply txid and empty array - assert_raises_rpc_error(-3, "not of expected type bool", self.nodes[n].getrawtransaction, txId, []) + assert_raises_rpc_error(-3, "not of expected type number", self.nodes[n].getrawtransaction, txId, []) # 8. invalid parameters - supply txid and empty dict - assert_raises_rpc_error(-3, "not of expected type bool", self.nodes[n].getrawtransaction, txId, {}) + assert_raises_rpc_error(-3, "not of expected type number", self.nodes[n].getrawtransaction, txId, {}) # Make a tx by sending, then generate 2 blocks; block1 has the tx in it tx = self.wallet.send_self_transfer(from_node=self.nodes[2])['txid'] @@ -145,9 +149,10 @@ class RawTransactionsTest(BitcoinTestFramework): assert_equal(gottx['in_active_chain'], True) if n == 0: self.log.info("Test getrawtransaction with -txindex, without blockhash: 'in_active_chain' should be absent") - gottx = self.nodes[n].getrawtransaction(txid=tx, verbose=True) - assert_equal(gottx['txid'], tx) - assert 'in_active_chain' not in gottx + for v in [1,2]: + gottx = self.nodes[n].getrawtransaction(txid=tx, verbosity=v) + assert_equal(gottx['txid'], tx) + assert 'in_active_chain' not in gottx else: self.log.info("Test getrawtransaction without -txindex, without blockhash: expect the call to raise") assert_raises_rpc_error(-5, err_msg, self.nodes[n].getrawtransaction, txid=tx, verbose=True) @@ -172,6 +177,70 @@ class RawTransactionsTest(BitcoinTestFramework): block = self.nodes[0].getblock(self.nodes[0].getblockhash(0)) assert_raises_rpc_error(-5, "The genesis block coinbase is not considered an ordinary transaction", self.nodes[0].getrawtransaction, block['merkleroot']) + def getrawtransaction_verbosity_tests(self): + tx = self.wallet.send_self_transfer(from_node=self.nodes[1])['txid'] + [block1] = self.generate(self.nodes[1], 1) + fields = [ + 'blockhash', + 'blocktime', + 'confirmations', + 'hash', + 'hex', + 'in_active_chain', + 'locktime', + 'size', + 'time', + 'txid', + 'vin', + 'vout', + 'vsize', + 'weight', + ] + prevout_fields = [ + 'generated', + 'height', + 'value', + 'scriptPubKey', + ] + script_pub_key_fields = [ + 'address', + 'asm', + 'hex', + 'type', + ] + # node 0 & 2 with verbosity 1 & 2 + for n, v in product([0, 2], [1, 2]): + self.log.info(f"Test getrawtransaction_verbosity {v} {'with' if n == 0 else 'without'} -txindex, with blockhash") + gottx = self.nodes[n].getrawtransaction(txid=tx, verbosity=v, blockhash=block1) + missing_fields = set(fields).difference(gottx.keys()) + if missing_fields: + raise AssertionError(f"fields {', '.join(missing_fields)} are not in transaction") + + assert(len(gottx['vin']) > 0) + if v == 1: + assert('fee' not in gottx) + assert('prevout' not in gottx['vin'][0]) + if v == 2: + assert(isinstance(gottx['fee'], Decimal)) + assert('prevout' in gottx['vin'][0]) + prevout = gottx['vin'][0]['prevout'] + script_pub_key = prevout['scriptPubKey'] + + missing_fields = set(prevout_fields).difference(prevout.keys()) + if missing_fields: + raise AssertionError(f"fields {', '.join(missing_fields)} are not in transaction") + + missing_fields = set(script_pub_key_fields).difference(script_pub_key.keys()) + if missing_fields: + raise AssertionError(f"fields {', '.join(missing_fields)} are not in transaction") + + # check verbosity 2 without blockhash but with txindex + assert('fee' in self.nodes[0].getrawtransaction(txid=tx, verbosity=2)) + # check that coinbase has no fee or does not throw any errors for verbosity 2 + coin_base = self.nodes[1].getblock(block1)['tx'][0] + gottx = self.nodes[1].getrawtransaction(txid=coin_base, verbosity=2, blockhash=block1) + assert('fee' not in gottx) + def createrawtransaction_tests(self): self.log.info("Test createrawtransaction") # Test `createrawtransaction` required parameters |