Skip to content

Commit daf881d

Browse files
committed
Merge bitcoin#23319: rpc: Return fee and prevout (utxos) to getrawtransaction
f866971 rpc: Return fee and prevout(s) to getrawtransaction (Douglas Chimento) Pull request description: Add fee response in BTC to getrawtransaction bitcoin#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 f866971 aureleoules: ACK f866971 hernanmarino: re ACK f866971 pablomartin4btc: re-tACK f866971 Tree-SHA512: 591fdc285d74fa7803e04ad01c7b70bc20fac6b1369e7bd5b8e2cde9b750ea52d6c70d79225b74bef4f4bbc0fb960877778017184e146119da4a55f9593d1224
2 parents ffa32ab + f866971 commit daf881d

File tree

3 files changed

+164
-30
lines changed

3 files changed

+164
-30
lines changed

src/rpc/client.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
102102
{ "getchaintxstats", 0, "nblocks" },
103103
{ "gettransaction", 1, "include_watchonly" },
104104
{ "gettransaction", 2, "verbose" },
105+
{ "getrawtransaction", 1, "verbosity" },
105106
{ "getrawtransaction", 1, "verbose" },
106107
{ "createrawtransaction", 0, "inputs" },
107108
{ "createrawtransaction", 1, "outputs" },

src/rpc/rawtransaction.cpp

Lines changed: 88 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
#include <script/signingprovider.h>
3333
#include <script/standard.h>
3434
#include <uint256.h>
35+
#include <undo.h>
3536
#include <util/bip32.h>
3637
#include <util/check.h>
3738
#include <util/strencodings.h>
@@ -50,15 +51,17 @@ using node::FindCoins;
5051
using node::GetTransaction;
5152
using node::NodeContext;
5253
using node::PSBTAnalysis;
54+
using node::ReadBlockFromDisk;
55+
using node::UndoReadFromDisk;
5356

54-
static void TxToJSON(const CTransaction& tx, const uint256 hashBlock, UniValue& entry, Chainstate& active_chainstate)
57+
static void TxToJSON(const CTransaction& tx, const uint256 hashBlock, UniValue& entry, Chainstate& active_chainstate, const CTxUndo* txundo = nullptr, TxVerbosity verbosity = TxVerbosity::SHOW_TXID)
5558
{
5659
// Call into TxToUniv() in bitcoin-common to decode the transaction hex.
5760
//
5861
// Blockchain contextual information (confirmations and blocktime) is not
5962
// available to code in bitcoin-common, so we query them here and push the
6063
// data into the returned UniValue.
61-
TxToUniv(tx, /*block_hash=*/uint256(), entry, /*include_hex=*/true, RPCSerializationFlags());
64+
TxToUniv(tx, /*block_hash=*/uint256(), entry, /*include_hex=*/true, RPCSerializationFlags(), txundo, verbosity);
6265

6366
if (!hashBlock.IsNull()) {
6467
LOCK(cs_main);
@@ -166,26 +169,27 @@ static RPCHelpMan getrawtransaction()
166169
{
167170
return RPCHelpMan{
168171
"getrawtransaction",
169-
"Return the raw transaction data.\n"
170172

171-
"\nBy default, this call only returns a transaction if it is in the mempool. If -txindex is enabled\n"
173+
"By default, this call only returns a transaction if it is in the mempool. If -txindex is enabled\n"
172174
"and no blockhash argument is passed, it will return the transaction if it is in the mempool or any block.\n"
173175
"If a blockhash argument is passed, it will return the transaction if\n"
174-
"the specified block is available and the transaction is in that block.\n"
175-
"\nHint: Use gettransaction for wallet transactions.\n"
176+
"the specified block is available and the transaction is in that block.\n\n"
177+
"Hint: Use gettransaction for wallet transactions.\n\n"
176178

177-
"\nIf verbose is 'true', returns an Object with information about 'txid'.\n"
178-
"If verbose is 'false' or omitted, returns a string that is serialized, hex-encoded data for 'txid'.",
179+
"If verbosity is 0 or omitted, returns the serialized transaction as a hex-encoded string.\n"
180+
"If verbosity is 1, returns a JSON Object with information about transaction.\n"
181+
"If verbosity is 2, returns a JSON Object with information about transaction, including fee and prevout information.",
179182
{
180183
{"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The transaction id"},
181-
{"verbose", RPCArg::Type::BOOL, RPCArg::Default{false}, "If false, return a string, otherwise return a json object"},
184+
{"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"},
182185
{"blockhash", RPCArg::Type::STR_HEX, RPCArg::Optional::OMITTED_NAMED_ARG, "The block in which to look for the transaction"},
183186
},
184187
{
185-
RPCResult{"if verbose is not set or set to false",
186-
RPCResult::Type::STR, "data", "The serialized, hex-encoded data for 'txid'"
188+
RPCResult{"if verbosity is not set or set to 0",
189+
RPCResult::Type::STR, "data", "The serialized transaction as a hex-encoded string for 'txid'"
187190
},
188-
RPCResult{"if verbose is set to true",
191+
RPCResult{"if verbosity is set to 1",
192+
// When updating this documentation, update `decoderawtransaction` in the same way.
189193
RPCResult::Type::OBJ, "", "",
190194
Cat<std::vector<RPCResult>>(
191195
{
@@ -198,20 +202,47 @@ static RPCHelpMan getrawtransaction()
198202
},
199203
DecodeTxDoc(/*txid_field_doc=*/"The transaction id (same as provided)")),
200204
},
205+
RPCResult{"for verbosity = 2",
206+
RPCResult::Type::OBJ, "", "",
207+
{
208+
{RPCResult::Type::ELISION, "", "Same output as verbosity = 1"},
209+
{RPCResult::Type::NUM, "fee", /* optional */ true, "transaction fee in " + CURRENCY_UNIT + ", omitted if block undo data is not available"},
210+
{RPCResult::Type::ARR, "vin", "",
211+
{
212+
{RPCResult::Type::OBJ, "", /* optional */ true, "utxo being spent, omitted if block undo data is not available",
213+
{
214+
{RPCResult::Type::ELISION, "", "Same output as verbosity = 1"},
215+
{RPCResult::Type::OBJ, "prevout", "Only if undo information is available)",
216+
{
217+
{RPCResult::Type::BOOL, "generated", "Coinbase or not"},
218+
{RPCResult::Type::NUM, "height", "The height of the prevout"},
219+
{RPCResult::Type::STR_AMOUNT, "value", "The value in " + CURRENCY_UNIT},
220+
{RPCResult::Type::OBJ, "scriptPubKey", "",
221+
{
222+
{RPCResult::Type::STR, "asm", "Disassembly of the public key script"},
223+
{RPCResult::Type::STR, "desc", "Inferred descriptor for the output"},
224+
{RPCResult::Type::STR_HEX, "hex", "The raw public key script bytes, hex-encoded"},
225+
{RPCResult::Type::STR, "address", /*optional=*/true, "The Bitcoin address (only if a well-defined address exists)"},
226+
{RPCResult::Type::STR, "type", "The type (one of: " + GetAllOutputTypes() + ")"},
227+
}},
228+
}},
229+
}},
230+
}},
231+
}},
201232
},
202233
RPCExamples{
203234
HelpExampleCli("getrawtransaction", "\"mytxid\"")
204-
+ HelpExampleCli("getrawtransaction", "\"mytxid\" true")
205-
+ HelpExampleRpc("getrawtransaction", "\"mytxid\", true")
206-
+ HelpExampleCli("getrawtransaction", "\"mytxid\" false \"myblockhash\"")
207-
+ HelpExampleCli("getrawtransaction", "\"mytxid\" true \"myblockhash\"")
235+
+ HelpExampleCli("getrawtransaction", "\"mytxid\" 1")
236+
+ HelpExampleRpc("getrawtransaction", "\"mytxid\", 1")
237+
+ HelpExampleCli("getrawtransaction", "\"mytxid\" 0 \"myblockhash\"")
238+
+ HelpExampleCli("getrawtransaction", "\"mytxid\" 1 \"myblockhash\"")
239+
+ HelpExampleCli("getrawtransaction", "\"mytxid\" 2 \"myblockhash\"")
208240
},
209241
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
210242
{
211243
const NodeContext& node = EnsureAnyNodeContext(request.context);
212244
ChainstateManager& chainman = EnsureChainman(node);
213245

214-
bool in_active_chain = true;
215246
uint256 hash = ParseHashV(request.params[0], "parameter 1");
216247
const CBlockIndex* blockindex = nullptr;
217248

@@ -220,10 +251,14 @@ static RPCHelpMan getrawtransaction()
220251
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "The genesis block coinbase is not considered an ordinary transaction and cannot be retrieved");
221252
}
222253

223-
// Accept either a bool (true) or a num (>=1) to indicate verbose output.
224-
bool fVerbose = false;
254+
// Accept either a bool (true) or a num (>=0) to indicate verbosity.
255+
int verbosity{0};
225256
if (!request.params[1].isNull()) {
226-
fVerbose = request.params[1].isNum() ? (request.params[1].getInt<int>() != 0) : request.params[1].get_bool();
257+
if (request.params[1].isBool()) {
258+
verbosity = request.params[1].get_bool();
259+
} else {
260+
verbosity = request.params[1].getInt<int>();
261+
}
227262
}
228263

229264
if (!request.params[2].isNull()) {
@@ -234,7 +269,6 @@ static RPCHelpMan getrawtransaction()
234269
if (!blockindex) {
235270
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block hash not found");
236271
}
237-
in_active_chain = chainman.ActiveChain().Contains(blockindex);
238272
}
239273

240274
bool f_txindex_ready = false;
@@ -262,13 +296,43 @@ static RPCHelpMan getrawtransaction()
262296
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, errmsg + ". Use gettransaction for wallet transactions.");
263297
}
264298

265-
if (!fVerbose) {
299+
if (verbosity <= 0) {
266300
return EncodeHexTx(*tx, RPCSerializationFlags());
267301
}
268302

269303
UniValue result(UniValue::VOBJ);
270-
if (blockindex) result.pushKV("in_active_chain", in_active_chain);
271-
TxToJSON(*tx, hash_block, result, chainman.ActiveChainstate());
304+
if (blockindex) {
305+
LOCK(cs_main);
306+
result.pushKV("in_active_chain", chainman.ActiveChain().Contains(blockindex));
307+
}
308+
// If request is verbosity >= 1 but no blockhash was given, then look up the blockindex
309+
if (request.params[2].isNull()) {
310+
LOCK(cs_main);
311+
blockindex = chainman.m_blockman.LookupBlockIndex(hash_block);
312+
}
313+
if (verbosity == 1) {
314+
TxToJSON(*tx, hash_block, result, chainman.ActiveChainstate());
315+
return result;
316+
}
317+
318+
CBlockUndo blockUndo;
319+
CBlock block;
320+
const bool is_block_pruned{WITH_LOCK(cs_main, return chainman.m_blockman.IsBlockPruned(blockindex))};
321+
322+
if (tx->IsCoinBase() ||
323+
!blockindex || is_block_pruned ||
324+
!(UndoReadFromDisk(blockUndo, blockindex) && ReadBlockFromDisk(block, blockindex, Params().GetConsensus()))) {
325+
TxToJSON(*tx, hash_block, result, chainman.ActiveChainstate());
326+
return result;
327+
}
328+
329+
CTxUndo* undoTX {nullptr};
330+
auto it = std::find_if(block.vtx.begin(), block.vtx.end(), [tx](CTransactionRef t){ return *t == *tx; });
331+
if (it != block.vtx.end()) {
332+
// -1 as blockundo does not have coinbase tx
333+
undoTX = &blockUndo.vtxundo.at(it - block.vtx.begin() - 1);
334+
}
335+
TxToJSON(*tx, hash_block, result, chainman.ActiveChainstate(), undoTX, TxVerbosity::SHOW_DETAILS_AND_PREVOUT);
272336
return result;
273337
},
274338
};

test/functional/rpc_rawtransaction.py

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from collections import OrderedDict
1616
from decimal import Decimal
17+
from itertools import product
1718

1819
from test_framework.blocktools import COINBASE_MATURITY
1920
from test_framework.messages import (
@@ -81,6 +82,7 @@ def run_test(self):
8182
self.generate(self.nodes[0], COINBASE_MATURITY + 1)
8283

8384
self.getrawtransaction_tests()
85+
self.getrawtransaction_verbosity_tests()
8486
self.createrawtransaction_tests()
8587
self.sendrawtransaction_tests()
8688
self.sendrawtransaction_testmempoolaccept_tests()
@@ -116,6 +118,7 @@ def getrawtransaction_tests(self):
116118
# 4. valid parameters - supply txid and 1 for verbose.
117119
# We only check the "hex" field of the output so we don't need to update this test every time the output format changes.
118120
assert_equal(self.nodes[n].getrawtransaction(txId, 1)["hex"], tx['hex'])
121+
assert_equal(self.nodes[n].getrawtransaction(txId, 2)["hex"], tx['hex'])
119122

120123
# 5. valid parameters - supply txid and True for non-verbose
121124
assert_equal(self.nodes[n].getrawtransaction(txId, True)["hex"], tx['hex'])
@@ -126,13 +129,14 @@ def getrawtransaction_tests(self):
126129

127130
# 6. invalid parameters - supply txid and invalid boolean values (strings) for verbose
128131
for value in ["True", "False"]:
129-
assert_raises_rpc_error(-3, "not of expected type bool", self.nodes[n].getrawtransaction, txid=txId, verbose=value)
132+
assert_raises_rpc_error(-3, "not of expected type number", self.nodes[n].getrawtransaction, txid=txId, verbose=value)
133+
assert_raises_rpc_error(-3, "not of expected type number", self.nodes[n].getrawtransaction, txid=txId, verbosity=value)
130134

131135
# 7. invalid parameters - supply txid and empty array
132-
assert_raises_rpc_error(-3, "not of expected type bool", self.nodes[n].getrawtransaction, txId, [])
136+
assert_raises_rpc_error(-3, "not of expected type number", self.nodes[n].getrawtransaction, txId, [])
133137

134138
# 8. invalid parameters - supply txid and empty dict
135-
assert_raises_rpc_error(-3, "not of expected type bool", self.nodes[n].getrawtransaction, txId, {})
139+
assert_raises_rpc_error(-3, "not of expected type number", self.nodes[n].getrawtransaction, txId, {})
136140

137141
# Make a tx by sending, then generate 2 blocks; block1 has the tx in it
138142
tx = self.wallet.send_self_transfer(from_node=self.nodes[2])['txid']
@@ -145,9 +149,10 @@ def getrawtransaction_tests(self):
145149
assert_equal(gottx['in_active_chain'], True)
146150
if n == 0:
147151
self.log.info("Test getrawtransaction with -txindex, without blockhash: 'in_active_chain' should be absent")
148-
gottx = self.nodes[n].getrawtransaction(txid=tx, verbose=True)
149-
assert_equal(gottx['txid'], tx)
150-
assert 'in_active_chain' not in gottx
152+
for v in [1,2]:
153+
gottx = self.nodes[n].getrawtransaction(txid=tx, verbosity=v)
154+
assert_equal(gottx['txid'], tx)
155+
assert 'in_active_chain' not in gottx
151156
else:
152157
self.log.info("Test getrawtransaction without -txindex, without blockhash: expect the call to raise")
153158
assert_raises_rpc_error(-5, err_msg, self.nodes[n].getrawtransaction, txid=tx, verbose=True)
@@ -172,6 +177,70 @@ def getrawtransaction_tests(self):
172177
block = self.nodes[0].getblock(self.nodes[0].getblockhash(0))
173178
assert_raises_rpc_error(-5, "The genesis block coinbase is not considered an ordinary transaction", self.nodes[0].getrawtransaction, block['merkleroot'])
174179

180+
def getrawtransaction_verbosity_tests(self):
181+
tx = self.wallet.send_self_transfer(from_node=self.nodes[1])['txid']
182+
[block1] = self.generate(self.nodes[1], 1)
183+
fields = [
184+
'blockhash',
185+
'blocktime',
186+
'confirmations',
187+
'hash',
188+
'hex',
189+
'in_active_chain',
190+
'locktime',
191+
'size',
192+
'time',
193+
'txid',
194+
'vin',
195+
'vout',
196+
'vsize',
197+
'weight',
198+
]
199+
prevout_fields = [
200+
'generated',
201+
'height',
202+
'value',
203+
'scriptPubKey',
204+
]
205+
script_pub_key_fields = [
206+
'address',
207+
'asm',
208+
'hex',
209+
'type',
210+
]
211+
# node 0 & 2 with verbosity 1 & 2
212+
for n, v in product([0, 2], [1, 2]):
213+
self.log.info(f"Test getrawtransaction_verbosity {v} {'with' if n == 0 else 'without'} -txindex, with blockhash")
214+
gottx = self.nodes[n].getrawtransaction(txid=tx, verbosity=v, blockhash=block1)
215+
missing_fields = set(fields).difference(gottx.keys())
216+
if missing_fields:
217+
raise AssertionError(f"fields {', '.join(missing_fields)} are not in transaction")
218+
219+
assert(len(gottx['vin']) > 0)
220+
if v == 1:
221+
assert('fee' not in gottx)
222+
assert('prevout' not in gottx['vin'][0])
223+
if v == 2:
224+
assert(isinstance(gottx['fee'], Decimal))
225+
assert('prevout' in gottx['vin'][0])
226+
prevout = gottx['vin'][0]['prevout']
227+
script_pub_key = prevout['scriptPubKey']
228+
229+
missing_fields = set(prevout_fields).difference(prevout.keys())
230+
if missing_fields:
231+
raise AssertionError(f"fields {', '.join(missing_fields)} are not in transaction")
232+
233+
missing_fields = set(script_pub_key_fields).difference(script_pub_key.keys())
234+
if missing_fields:
235+
raise AssertionError(f"fields {', '.join(missing_fields)} are not in transaction")
236+
237+
# check verbosity 2 without blockhash but with txindex
238+
assert('fee' in self.nodes[0].getrawtransaction(txid=tx, verbosity=2))
239+
# check that coinbase has no fee or does not throw any errors for verbosity 2
240+
coin_base = self.nodes[1].getblock(block1)['tx'][0]
241+
gottx = self.nodes[1].getrawtransaction(txid=coin_base, verbosity=2, blockhash=block1)
242+
assert('fee' not in gottx)
243+
175244
def createrawtransaction_tests(self):
176245
self.log.info("Test createrawtransaction")
177246
# Test `createrawtransaction` required parameters

0 commit comments

Comments
 (0)