rpc: extend TxDoc() for getblock verbosity 2/3

Add fee, hex, fee_doc, prevout_doc, and vin_item_doc
options to TxDocOptions, and use them to describe
getblock verbosity=2/3 with structured metadata instead
of inline ELISION-based reuse.

Add GetBlockFields() to share the block-level result layout
across verbosity levels.
This commit is contained in:
Renato Britto
2026-03-17 00:27:31 -03:00
committed by satsfy (Renato Britto)
parent 0380a1c46b
commit 372ac283ac
3 changed files with 118 additions and 91 deletions

View File

@@ -33,6 +33,7 @@
#include <node/utxo_snapshot.h>
#include <node/warnings.h>
#include <primitives/transaction.h>
#include <rpc/rawtransaction_util.h>
#include <rpc/server.h>
#include <rpc/server_util.h>
#include <rpc/util.h>
@@ -743,32 +744,62 @@ static CBlockUndo GetUndoChecked(BlockManager& blockman, const CBlockIndex& bloc
return blockUndo;
}
const RPCResult& GetBlockVin()
static std::vector<RPCResult> GetBlockFields(RPCResult tx_result, std::optional<std::string> elision_msg = std::nullopt)
{
static const RPCResult getblock_vin{
RPCResult::Type::ARR, "vin", "",
auto fields = std::vector<RPCResult>{
{RPCResult::Type::STR_HEX, "hash", "the block hash (same as provided)"},
{RPCResult::Type::NUM, "confirmations", "The number of confirmations, or -1 if the block is not on the main chain"},
{RPCResult::Type::NUM, "size", "The block size"},
{RPCResult::Type::NUM, "strippedsize", "The block size excluding witness data"},
{RPCResult::Type::NUM, "weight", "The block weight as defined in BIP 141"},
{RPCResult::Type::OBJ, "coinbase_tx", "Coinbase transaction metadata",
{
{RPCResult::Type::OBJ, "", "",
{
{RPCResult::Type::ELISION, "", "The same output as verbosity = 2"},
{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 output script"},
{RPCResult::Type::STR, "desc", "Inferred descriptor for the output"},
{RPCResult::Type::STR_HEX, "hex", "The raw output 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() + ")"},
}},
}},
}},
}
{RPCResult::Type::NUM, "version", "The coinbase transaction version"},
{RPCResult::Type::NUM, "locktime", "The coinbase transaction's locktime (nLockTime)"},
{RPCResult::Type::NUM, "sequence", "The coinbase input's sequence number (nSequence)"},
{RPCResult::Type::STR_HEX, "coinbase", "The coinbase input's script"},
{RPCResult::Type::STR_HEX, "witness", /*optional=*/true, "The coinbase input's first (and only) witness stack element, if present"},
}},
{RPCResult::Type::NUM, "height", "The block height or index"},
{RPCResult::Type::NUM, "version", "The block version"},
{RPCResult::Type::STR_HEX, "versionHex", "The block version formatted in hexadecimal"},
{RPCResult::Type::STR_HEX, "merkleroot", "The merkle root"},
};
return getblock_vin;
fields.push_back(std::move(tx_result));
fields.emplace_back(RPCResult::Type::NUM_TIME, "time", "The block time expressed in " + UNIX_EPOCH_TIME);
fields.emplace_back(RPCResult::Type::NUM_TIME, "mediantime", "The median block time expressed in " + UNIX_EPOCH_TIME);
fields.emplace_back(RPCResult::Type::NUM, "nonce", "The nonce");
fields.emplace_back(RPCResult::Type::STR_HEX, "bits", "nBits: compact representation of the block difficulty target");
fields.emplace_back(RPCResult::Type::STR_HEX, "target", "The difficulty target");
fields.emplace_back(RPCResult::Type::NUM, "difficulty", "The difficulty");
fields.emplace_back(RPCResult::Type::STR_HEX, "chainwork", "Expected number of hashes required to produce the chain up to this block (in hex)");
fields.emplace_back(RPCResult::Type::NUM, "nTx", "The number of transactions in the block");
fields.emplace_back(RPCResult::Type::STR_HEX, "previousblockhash", /*optional=*/true, "The hash of the previous block (if available)");
fields.emplace_back(RPCResult::Type::STR_HEX, "nextblockhash", /*optional=*/true, "The hash of the next block (if available)");
if (elision_msg) {
// Elide all block-level fields except the tx array (which differs per verbosity)
std::vector<RPCResult> new_fields;
new_fields.reserve(fields.size());
bool first = true;
for (const auto& f : fields) {
if (f.m_key_name == "tx") {
new_fields.push_back(f);
continue;
}
if (first) {
RPCResultOptions eopts = f.m_opts;
eopts.print_elision = *elision_msg;
new_fields.emplace_back(f, std::move(eopts));
first = false;
} else {
RPCResultOptions eopts = f.m_opts;
eopts.print_elision = HelpElisionSkip{};
new_fields.emplace_back(f, std::move(eopts));
}
}
fields = std::move(new_fields);
}
return fields;
}
static RPCMethod getblock()
@@ -785,67 +816,33 @@ static RPCMethod getblock()
RPCArgOptions{.skip_type_check = true}},
},
{
RPCResult{"for verbosity = 0",
RPCResult::Type::STR_HEX, "", "A string that is serialized, hex-encoded data for block 'hash'"},
RPCResult{"for verbosity = 1",
RPCResult::Type::OBJ, "", "",
{
{RPCResult::Type::STR_HEX, "hash", "the block hash (same as provided)"},
{RPCResult::Type::NUM, "confirmations", "The number of confirmations, or -1 if the block is not on the main chain"},
{RPCResult::Type::NUM, "size", "The block size"},
{RPCResult::Type::NUM, "strippedsize", "The block size excluding witness data"},
{RPCResult::Type::NUM, "weight", "The block weight as defined in BIP 141"},
{RPCResult::Type::OBJ, "coinbase_tx", "Coinbase transaction metadata",
{
{RPCResult::Type::NUM, "version", "The coinbase transaction version"},
{RPCResult::Type::NUM, "locktime", "The coinbase transaction's locktime (nLockTime)"},
{RPCResult::Type::NUM, "sequence", "The coinbase input's sequence number (nSequence)"},
{RPCResult::Type::STR_HEX, "coinbase", "The coinbase input's script"},
{RPCResult::Type::STR_HEX, "witness", /*optional=*/true, "The coinbase input's first (and only) witness stack element, if present"},
}},
{RPCResult::Type::NUM, "height", "The block height or index"},
{RPCResult::Type::NUM, "version", "The block version"},
{RPCResult::Type::STR_HEX, "versionHex", "The block version formatted in hexadecimal"},
{RPCResult::Type::STR_HEX, "merkleroot", "The merkle root"},
{RPCResult::Type::ARR, "tx", "The transaction ids",
{{RPCResult::Type::STR_HEX, "", "The transaction id"}}},
{RPCResult::Type::NUM_TIME, "time", "The block time expressed in " + UNIX_EPOCH_TIME},
{RPCResult::Type::NUM_TIME, "mediantime", "The median block time expressed in " + UNIX_EPOCH_TIME},
{RPCResult::Type::NUM, "nonce", "The nonce"},
{RPCResult::Type::STR_HEX, "bits", "nBits: compact representation of the block difficulty target"},
{RPCResult::Type::STR_HEX, "target", "The difficulty target"},
{RPCResult::Type::NUM, "difficulty", "The difficulty"},
{RPCResult::Type::STR_HEX, "chainwork", "Expected number of hashes required to produce the chain up to this block (in hex)"},
{RPCResult::Type::NUM, "nTx", "The number of transactions in the block"},
{RPCResult::Type::STR_HEX, "previousblockhash", /*optional=*/true, "The hash of the previous block (if available)"},
{RPCResult::Type::STR_HEX, "nextblockhash", /*optional=*/true, "The hash of the next block (if available)"},
}},
RPCResult{"for verbosity = 2",
RPCResult::Type::OBJ, "", "",
{
{RPCResult::Type::ELISION, "", "Same output as verbosity = 1"},
{RPCResult::Type::ARR, "tx", "",
{
{RPCResult::Type::OBJ, "", "",
RPCResult{"for verbosity = 0", RPCResult::Type::STR_HEX, "", "A string that is serialized, hex-encoded data for block 'hash'"},
RPCResult{"for verbosity = 1", RPCResult::Type::OBJ, "", "",
GetBlockFields({RPCResult::Type::ARR, "tx", "The transaction ids",
{{RPCResult::Type::STR_HEX, "", "The transaction id"}}})},
RPCResult{"for verbosity = 2", RPCResult::Type::OBJ, "", "",
GetBlockFields({RPCResult::Type::ARR, "tx", "",
{
{RPCResult::Type::ELISION, "", "The transactions in the format of the getrawtransaction RPC. Different from verbosity = 1 \"tx\" result"},
{RPCResult::Type::NUM, "fee", /*optional=*/true, "The transaction fee in " + CURRENCY_UNIT + ", omitted if block undo data is not available"},
}},
}},
}},
RPCResult{"for verbosity = 3",
RPCResult::Type::OBJ, "", "",
{
{RPCResult::Type::ELISION, "", "Same output as verbosity = 2"},
{RPCResult::Type::ARR, "tx", "",
{
{RPCResult::Type::OBJ, "", "",
{RPCResult::Type::OBJ, "", "",
TxDoc({.elision_mode = ElisionMode::WithSummary,
.elision_summary = "The transactions in the format of the getrawtransaction RPC. Different from verbosity = 1 \"tx\" result",
.fee = true, .hex = true,
.fee_doc = "The transaction fee in " + CURRENCY_UNIT + ", omitted if block undo data is not available"})},
}}, /*elision_msg=*/"Same output as verbosity = 1")},
RPCResult{"for verbosity = 3", RPCResult::Type::OBJ, "", "",
GetBlockFields({RPCResult::Type::ARR, "tx", "",
{
GetBlockVin(),
}},
}},
}},
},
{RPCResult::Type::OBJ, "", "",
TxDoc({.elision_mode = ElisionMode::Silent,
.prevout = true,
.prevout_optional = true,
.fee = true,
.hex = true,
.vin_item_doc = "",
.prevout_doc = "(Only if undo information is available)",
.vin_inner_elision = "The same output as verbosity = 2"})},
}}, /*elision_msg=*/"Same output as verbosity = 2")},
},
RPCExamples{
HelpExampleCli("getblock", "\"00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09\"")
+ HelpExampleRpc("getblock", "\"00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09\"")

View File

@@ -17,6 +17,7 @@
#include <script/signingprovider.h>
#include <tinyformat.h>
#include <univalue.h>
#include <util/check.h>
#include <util/rbf.h>
#include <util/string.h>
#include <util/strencodings.h>
@@ -346,6 +347,17 @@ void SignTransactionResultToJSON(CMutableTransaction& mtx, bool complete, const
std::vector<RPCResult> TxDoc(const TxDocOptions& opts)
{
CHECK_NONFATAL(!opts.fee_doc || opts.fee);
CHECK_NONFATAL(!opts.prevout_doc || opts.prevout);
CHECK_NONFATAL(!opts.vin_item_doc || opts.vin_inner_elision);
CHECK_NONFATAL(opts.elision_mode != ElisionMode::WithSummary || opts.elision_summary.has_value());
const std::string fee_doc{opts.fee_doc.value_or(
"transaction fee in " + CURRENCY_UNIT + ", omitted if block undo data is not available")};
const std::string prevout_doc{opts.prevout_doc.value_or(
"The previous output, omitted if block undo data is not available")};
const std::string vin_item_doc{opts.vin_item_doc.value_or("utxo being spent")};
auto vin_inner = std::vector<RPCResult>{
{RPCResult::Type::STR_HEX, "coinbase", /*optional=*/true, "The coinbase value (only if coinbase transaction)"},
{RPCResult::Type::STR_HEX, "txid", /*optional=*/true, "The transaction id (if not coinbase transaction)"},
@@ -361,14 +373,15 @@ std::vector<RPCResult> TxDoc(const TxDocOptions& opts)
}},
};
if (opts.prevout) {
vin_inner.emplace_back(RPCResult::Type::OBJ, "prevout", opts.prevout_optional,
"The previous output, omitted if block undo data is not available",
std::vector<RPCResult>{
{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", "", ScriptPubKeyDoc()},
});
vin_inner.emplace_back(
RPCResult::Type::OBJ, "prevout", opts.prevout_optional, prevout_doc,
std::vector<RPCResult>{
{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", "", ScriptPubKeyDoc()},
}
);
}
vin_inner.emplace_back(RPCResult::Type::NUM, "sequence", "The script sequence number");
@@ -401,7 +414,7 @@ std::vector<RPCResult> TxDoc(const TxDocOptions& opts)
{RPCResult::Type::NUM_TIME, "locktime", "The lock time"},
{RPCResult::Type::ARR, "vin", "",
{
{RPCResult::Type::OBJ, "", "", std::move(vin_inner)},
{RPCResult::Type::OBJ, "", opts.vin_inner_elision ? vin_item_doc : "", std::move(vin_inner)},
}},
{RPCResult::Type::ARR, "vout", "",
{
@@ -418,12 +431,19 @@ std::vector<RPCResult> TxDoc(const TxDocOptions& opts)
}},
};
if (opts.fee) fields.emplace_back(RPCResult::Type::NUM, "fee", /*optional=*/true, fee_doc);
if (opts.hex) fields.emplace_back(RPCResult::Type::STR_HEX, "hex", "The hex-encoded transaction data");
if (opts.elision_mode != ElisionMode::None) {
const bool silent = opts.elision_mode == ElisionMode::Silent;
std::vector<RPCResult> new_fields;
new_fields.reserve(fields.size());
bool first = true;
for (const auto& f : fields) {
if (!silent && f.m_key_name == "fee") {
new_fields.push_back(f);
continue;
}
if (f.m_key_name == "vin" && opts.vin_inner_elision) {
new_fields.push_back(f);
continue;

View File

@@ -75,6 +75,16 @@ struct TxDocOptions {
bool prevout{false};
/// Mark prevout field as optional (omitted when undo data unavailable)
bool prevout_optional{false};
/// Include fee field
bool fee{false};
/// Include hex field
bool hex{false};
/// Customize the vin item object's description (only meaningful when vin_inner_elision is set)
std::optional<std::string> vin_item_doc{};
/// Customize the prevout field's description (only meaningful when prevout is true)
std::optional<std::string> prevout_doc{};
/// Customize the fee field's description (only meaningful when fee is true)
std::optional<std::string> fee_doc{};
/// Elide vin inner fields but keep vin array with prevout expanded.
std::optional<std::string> vin_inner_elision{};
};