rpc: Return fee and prevout(s) to getrawtransaction

* Add optional fee response in BTC to getrawtransaction
* Add optional prevout(s) response to getrawtransaction showing utxos being spent
* Add getrawtransaction_verbosity functional test to validate fields
pull/23319/head
Douglas Chimento 3 years ago
parent bfce05cc34
commit f86697163e
No known key found for this signature in database
GPG Key ID: D1AD6668EB9BC5A9

@ -102,6 +102,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
{ "getchaintxstats", 0, "nblocks" }, { "getchaintxstats", 0, "nblocks" },
{ "gettransaction", 1, "include_watchonly" }, { "gettransaction", 1, "include_watchonly" },
{ "gettransaction", 2, "verbose" }, { "gettransaction", 2, "verbose" },
{ "getrawtransaction", 1, "verbosity" },
{ "getrawtransaction", 1, "verbose" }, { "getrawtransaction", 1, "verbose" },
{ "createrawtransaction", 0, "inputs" }, { "createrawtransaction", 0, "inputs" },
{ "createrawtransaction", 1, "outputs" }, { "createrawtransaction", 1, "outputs" },

@ -32,6 +32,7 @@
#include <script/signingprovider.h> #include <script/signingprovider.h>
#include <script/standard.h> #include <script/standard.h>
#include <uint256.h> #include <uint256.h>
#include <undo.h>
#include <util/bip32.h> #include <util/bip32.h>
#include <util/check.h> #include <util/check.h>
#include <util/strencodings.h> #include <util/strencodings.h>
@ -50,15 +51,17 @@ using node::FindCoins;
using node::GetTransaction; using node::GetTransaction;
using node::NodeContext; using node::NodeContext;
using node::PSBTAnalysis; 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. // Call into TxToUniv() in bitcoin-common to decode the transaction hex.
// //
// Blockchain contextual information (confirmations and blocktime) is not // Blockchain contextual information (confirmations and blocktime) is not
// available to code in bitcoin-common, so we query them here and push the // available to code in bitcoin-common, so we query them here and push the
// data into the returned UniValue. // 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()) { if (!hashBlock.IsNull()) {
LOCK(cs_main); LOCK(cs_main);
@ -166,26 +169,27 @@ static RPCHelpMan getrawtransaction()
{ {
return RPCHelpMan{ return RPCHelpMan{
"getrawtransaction", "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" "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" "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" "the specified block is available and the transaction is in that block.\n\n"
"\nHint: Use gettransaction for wallet transactions.\n" "Hint: Use gettransaction for wallet transactions.\n\n"
"\nIf verbose is 'true', returns an Object with information about 'txid'.\n" "If verbosity is 0 or omitted, returns the serialized transaction as a hex-encoded string.\n"
"If verbose is 'false' or omitted, returns a string that is serialized, hex-encoded data for 'txid'.", "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"}, {"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"}, {"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{"if verbosity is not set or set to 0",
RPCResult::Type::STR, "data", "The serialized, hex-encoded data for 'txid'" 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, "", "", RPCResult::Type::OBJ, "", "",
Cat<std::vector<RPCResult>>( Cat<std::vector<RPCResult>>(
{ {
@ -198,20 +202,47 @@ static RPCHelpMan getrawtransaction()
}, },
DecodeTxDoc(/*txid_field_doc=*/"The transaction id (same as provided)")), 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{ RPCExamples{
HelpExampleCli("getrawtransaction", "\"mytxid\"") HelpExampleCli("getrawtransaction", "\"mytxid\"")
+ HelpExampleCli("getrawtransaction", "\"mytxid\" true") + HelpExampleCli("getrawtransaction", "\"mytxid\" 1")
+ HelpExampleRpc("getrawtransaction", "\"mytxid\", true") + HelpExampleRpc("getrawtransaction", "\"mytxid\", 1")
+ HelpExampleCli("getrawtransaction", "\"mytxid\" false \"myblockhash\"") + HelpExampleCli("getrawtransaction", "\"mytxid\" 0 \"myblockhash\"")
+ HelpExampleCli("getrawtransaction", "\"mytxid\" true \"myblockhash\"") + HelpExampleCli("getrawtransaction", "\"mytxid\" 1 \"myblockhash\"")
+ HelpExampleCli("getrawtransaction", "\"mytxid\" 2 \"myblockhash\"")
}, },
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
{ {
const NodeContext& node = EnsureAnyNodeContext(request.context); const NodeContext& node = EnsureAnyNodeContext(request.context);
ChainstateManager& chainman = EnsureChainman(node); ChainstateManager& chainman = EnsureChainman(node);
bool in_active_chain = true;
uint256 hash = ParseHashV(request.params[0], "parameter 1"); uint256 hash = ParseHashV(request.params[0], "parameter 1");
const CBlockIndex* blockindex = nullptr; 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"); 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. // Accept either a bool (true) or a num (>=0) to indicate verbosity.
bool fVerbose = false; int verbosity{0};
if (!request.params[1].isNull()) { 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()) { if (!request.params[2].isNull()) {
@ -234,7 +269,6 @@ static RPCHelpMan getrawtransaction()
if (!blockindex) { if (!blockindex) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block hash not found"); throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block hash not found");
} }
in_active_chain = chainman.ActiveChain().Contains(blockindex);
} }
bool f_txindex_ready = false; 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."); throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, errmsg + ". Use gettransaction for wallet transactions.");
} }
if (!fVerbose) { if (verbosity <= 0) {
return EncodeHexTx(*tx, RPCSerializationFlags()); return EncodeHexTx(*tx, RPCSerializationFlags());
} }
UniValue result(UniValue::VOBJ); UniValue result(UniValue::VOBJ);
if (blockindex) result.pushKV("in_active_chain", in_active_chain); if (blockindex) {
TxToJSON(*tx, hash_block, result, chainman.ActiveChainstate()); 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; return result;
}, },
}; };

@ -14,6 +14,7 @@ Test the following RPCs:
from collections import OrderedDict from collections import OrderedDict
from decimal import Decimal from decimal import Decimal
from itertools import product
from test_framework.blocktools import COINBASE_MATURITY from test_framework.blocktools import COINBASE_MATURITY
from test_framework.messages import ( from test_framework.messages import (
@ -80,6 +81,7 @@ class RawTransactionsTest(BitcoinTestFramework):
self.generate(self.nodes[0], COINBASE_MATURITY + 1) self.generate(self.nodes[0], COINBASE_MATURITY + 1)
self.getrawtransaction_tests() self.getrawtransaction_tests()
self.getrawtransaction_verbosity_tests()
self.createrawtransaction_tests() self.createrawtransaction_tests()
self.sendrawtransaction_tests() self.sendrawtransaction_tests()
self.sendrawtransaction_testmempoolaccept_tests() self.sendrawtransaction_testmempoolaccept_tests()
@ -114,6 +116,7 @@ class RawTransactionsTest(BitcoinTestFramework):
# 4. valid parameters - supply txid and 1 for verbose. # 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. # 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, 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 # 5. valid parameters - supply txid and True for non-verbose
assert_equal(self.nodes[n].getrawtransaction(txId, True)["hex"], tx['hex']) assert_equal(self.nodes[n].getrawtransaction(txId, True)["hex"], tx['hex'])
@ -124,13 +127,14 @@ class RawTransactionsTest(BitcoinTestFramework):
# 6. invalid parameters - supply txid and invalid boolean values (strings) for verbose # 6. invalid parameters - supply txid and invalid boolean values (strings) for verbose
for value in ["True", "False"]: 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 # 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 # 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 # 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'] tx = self.wallet.send_self_transfer(from_node=self.nodes[2])['txid']
@ -143,9 +147,10 @@ class RawTransactionsTest(BitcoinTestFramework):
assert_equal(gottx['in_active_chain'], True) assert_equal(gottx['in_active_chain'], True)
if n == 0: if n == 0:
self.log.info("Test getrawtransaction with -txindex, without blockhash: 'in_active_chain' should be absent") self.log.info("Test getrawtransaction with -txindex, without blockhash: 'in_active_chain' should be absent")
gottx = self.nodes[n].getrawtransaction(txid=tx, verbose=True) for v in [1,2]:
assert_equal(gottx['txid'], tx) gottx = self.nodes[n].getrawtransaction(txid=tx, verbosity=v)
assert 'in_active_chain' not in gottx assert_equal(gottx['txid'], tx)
assert 'in_active_chain' not in gottx
else: else:
self.log.info("Test getrawtransaction without -txindex, without blockhash: expect the call to raise") 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) assert_raises_rpc_error(-5, err_msg, self.nodes[n].getrawtransaction, txid=tx, verbose=True)
@ -170,6 +175,70 @@ class RawTransactionsTest(BitcoinTestFramework):
block = self.nodes[0].getblock(self.nodes[0].getblockhash(0)) 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']) 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): def createrawtransaction_tests(self):
self.log.info("Test createrawtransaction") self.log.info("Test createrawtransaction")
# Test `createrawtransaction` required parameters # Test `createrawtransaction` required parameters

Loading…
Cancel
Save