rpc: extend TxDoc() for getrawtransaction verbosity 2
What changed, and why it matters
This commit is a code cleanup for Bitcoin Core's RPC help documentation. It restructures how the help text for getrawtransaction verbosity=2 is generated, making it more consistent with other RPC documentation. It does not change how transactions are fetched, validated, or returned at runtime. There is no indication this fixes a security bug.
No security action required. Treat as a normal refactoring/documentation improvement during review.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch extends TxDocOptions and TxDoc() in src/rpc/rawtransaction_util.{h,cpp} to support structured elision modes (None, WithSummary, Silent), a prevout field, and vin_inner_elision. It then refactors getrawtransaction verbosity=2 help in src/rpc/rawtransaction.cpp to reuse verbosity=1 block fields via ElideGroup and TxDoc, replacing a hand-written ELISION-based result description. src/rpc/util.cpp’s elision_has_description() is relaxed to accept any group with at least one non-SKIP field. The changes are confined to RPC help metadata generation; no consensus, networking, or runtime transaction logic is modified.
Changed components
src/rpc/rawtransaction.cppsrc/rpc/rawtransaction_util.cppsrc/rpc/rawtransaction_util.hsrc/rpc/util.cppInspect captured patch +110 / −59
diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp
index f00ee62d..618277d7 100644
--- a/src/rpc/rawtransaction.cpp
+++ b/src/rpc/rawtransaction.cpp
@@ -213,6 +213,23 @@ PartiallySignedTransaction ProcessPSBT(const std::string& psbt_string, const std
static RPCMethod getrawtransaction()
{
+ const std::vector<RPCResult> verbosity_1_block{
+ {RPCResult::Type::BOOL, "in_active_chain", /*optional=*/true, "Whether specified block is in the active chain or not (only present with explicit \"blockhash\" argument)"},
+ {RPCResult::Type::STR_HEX, "blockhash", /*optional=*/true, "the block hash"},
+ {RPCResult::Type::NUM, "confirmations", /*optional=*/true, "The confirmations"},
+ {RPCResult::Type::NUM_TIME, "blocktime", /*optional=*/true, "The block time expressed in " + UNIX_EPOCH_TIME},
+ {RPCResult::Type::NUM, "time", /*optional=*/true, "Same as \"blocktime\""},
+ {RPCResult::Type::STR_HEX, "hex", "The serialized, hex-encoded data for 'txid'"},
+ };
+ const auto v2_extras = Cat<std::vector<RPCResult>>(
+ std::vector<RPCResult>{{
+ RPCResult::Type::NUM, "fee", /*optional=*/true,
+ "transaction fee in " + CURRENCY_UNIT + ", omitted if block undo data is not available"
+ }},
+ TxDoc({.elision_mode = ElisionMode::Silent,
+ .prevout = true,
+ .prevout_optional = true,
+ .vin_inner_elision = "Same vin fields as verbosity = 1"}));
return RPCMethod{
"getrawtransaction",
@@ -238,36 +255,11 @@ static RPCMethod getrawtransaction()
RPCResult{"if verbosity is set to 1",
RPCResult::Type::OBJ, "", "",
Cat<std::vector<RPCResult>>(
- {
- {RPCResult::Type::BOOL, "in_active_chain", /*optional=*/true, "Whether specified block is in the active chain or not (only present with explicit \"blockhash\" argument)"},
- {RPCResult::Type::STR_HEX, "blockhash", /*optional=*/true, "the block hash"},
- {RPCResult::Type::NUM, "confirmations", /*optional=*/true, "The confirmations"},
- {RPCResult::Type::NUM_TIME, "blocktime", /*optional=*/true, "The block time expressed in " + UNIX_EPOCH_TIME},
- {RPCResult::Type::NUM, "time", /*optional=*/true, "Same as \"blocktime\""},
- {RPCResult::Type::STR_HEX, "hex", "The serialized, hex-encoded data for 'txid'"},
- },
+ verbosity_1_block,
TxDoc({.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, "", "utxo being spent",
- {
- {RPCResult::Type::ELISION, "", "Same output as verbosity = 1"},
- {RPCResult::Type::OBJ, "prevout", /*optional=*/true, "The previous output, omitted if block undo data is not 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", "", ScriptPubKeyDoc()},
- }},
- }},
- }},
- }},
+ RPCResult{"for verbosity = 2", RPCResult::Type::OBJ, "", "",
+ Cat(ElideGroup(verbosity_1_block, "Same output as verbosity = 1"), v2_extras)},
},
RPCExamples{
HelpExampleCli("getrawtransaction", "\"mytxid\"")
@@ -799,7 +791,7 @@ const RPCResult& DecodePSBTInputs()
{RPCResult::Type::OBJ, "", "",
{
{RPCResult::Type::OBJ, "non_witness_utxo", /*optional=*/true, "Decoded network transaction for non-witness UTXOs",
- TxDoc({.elision_description="The layout is the same as the output of decoderawtransaction."})
+ TxDoc({.elision_mode = ElisionMode::WithSummary, .elision_summary = "The layout is the same as the output of decoderawtransaction."})
},
{RPCResult::Type::OBJ, "witness_utxo", /*optional=*/true, "Transaction output for witness UTXOs",
{
@@ -1055,7 +1047,7 @@ static RPCMethod decodepsbt()
RPCResult::Type::OBJ, "", "",
{
{RPCResult::Type::OBJ, "tx", /*optional=*/true, "The decoded network-serialized unsigned transaction.",
- TxDoc({.elision_description="The layout is the same as the output of decoderawtransaction."})
+ TxDoc({.elision_mode = ElisionMode::WithSummary, .elision_summary = "The layout is the same as the output of decoderawtransaction."})
},
{RPCResult::Type::ARR, "global_xpubs", "",
{
diff --git a/src/rpc/rawtransaction_util.cpp b/src/rpc/rawtransaction_util.cpp
index 4f9f7b5c..a1973342 100644
--- a/src/rpc/rawtransaction_util.cpp
+++ b/src/rpc/rawtransaction_util.cpp
@@ -346,6 +346,51 @@ void SignTransactionResultToJSON(CMutableTransaction& mtx, bool complete, const
std::vector<RPCResult> TxDoc(const TxDocOptions& opts)
{
+ 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)"},
+ {RPCResult::Type::NUM, "vout", /*optional=*/true, "The output number (if not coinbase transaction)"},
+ {RPCResult::Type::OBJ, "scriptSig", /*optional=*/true, "The script (if not coinbase transaction)",
+ {
+ {RPCResult::Type::STR, "asm", "Disassembly of the signature script"},
+ {RPCResult::Type::STR_HEX, "hex", "The raw signature script bytes, hex-encoded"},
+ }},
+ {RPCResult::Type::ARR, "txinwitness", /*optional=*/true, "",
+ {
+ {RPCResult::Type::STR_HEX, "hex", "hex-encoded witness data (if any)"},
+ }},
+ };
+ 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::NUM, "sequence", "The script sequence number");
+
+ if (opts.vin_inner_elision) {
+ vin_inner = ElideGroup(std::move(vin_inner), *opts.vin_inner_elision);
+ if (opts.prevout) {
+ // prevout remains visible even when other fields are elided
+ std::vector<RPCResult> new_vin;
+ new_vin.reserve(vin_inner.size());
+ for (const auto& r : vin_inner) {
+ if (r.m_key_name == "prevout") {
+ RPCResultOptions unopts = r.m_opts;
+ unopts.print_elision = HelpElisionNone{};
+ new_vin.emplace_back(r, std::move(unopts));
+ } else {
+ new_vin.push_back(r);
+ }
+ }
+ vin_inner = std::move(new_vin);
+ }
+ }
+
auto fields = std::vector<RPCResult>{
{RPCResult::Type::STR_HEX, "txid", opts.txid_field_doc},
{RPCResult::Type::STR_HEX, "hash", "The transaction hash (differs from txid for witness transactions)"},
@@ -356,22 +401,7 @@ std::vector<RPCResult> TxDoc(const TxDocOptions& opts)
{RPCResult::Type::NUM_TIME, "locktime", "The lock time"},
{RPCResult::Type::ARR, "vin", "",
{
- {RPCResult::Type::OBJ, "", "",
- {
- {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)"},
- {RPCResult::Type::NUM, "vout", /*optional=*/true, "The output number (if not coinbase transaction)"},
- {RPCResult::Type::OBJ, "scriptSig", /*optional=*/true, "The script (if not coinbase transaction)",
- {
- {RPCResult::Type::STR, "asm", "Disassembly of the signature script"},
- {RPCResult::Type::STR_HEX, "hex", "The raw signature script bytes, hex-encoded"},
- }},
- {RPCResult::Type::ARR, "txinwitness", /*optional=*/true, "",
- {
- {RPCResult::Type::STR_HEX, "hex", "hex-encoded witness data (if any)"},
- }},
- {RPCResult::Type::NUM, "sequence", "The script sequence number"},
- }},
+ {RPCResult::Type::OBJ, "", "", std::move(vin_inner)},
}},
{RPCResult::Type::ARR, "vout", "",
{
@@ -388,8 +418,28 @@ std::vector<RPCResult> TxDoc(const TxDocOptions& opts)
}},
};
- if (opts.elision_description) {
- fields = ElideGroup(std::move(fields), *opts.elision_description);
+ 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 (f.m_key_name == "vin" && opts.vin_inner_elision) {
+ new_fields.push_back(f);
+ continue;
+ }
+ if (!silent && first) {
+ RPCResultOptions eopts = f.m_opts;
+ eopts.print_elision = opts.elision_summary.value_or("");
+ 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;
diff --git a/src/rpc/rawtransaction_util.h b/src/rpc/rawtransaction_util.h
index 5a1bc604..ee2f3040 100644
--- a/src/rpc/rawtransaction_util.h
+++ b/src/rpc/rawtransaction_util.h
@@ -56,13 +56,27 @@ void AddOutputs(CMutableTransaction& rawTx, const UniValue& outputs_in);
/** Create a transaction from univalue parameters */
CMutableTransaction ConstructTransaction(const UniValue& inputs_in, const UniValue& outputs_in, const UniValue& locktime, std::optional<bool> rbf, uint32_t version);
+enum class ElisionMode {
+ None, ///< no elision, all top-level fields rendered normally
+ WithSummary, ///< first field carries elision_summary as "...", rest skipped
+ Silent, ///< all top-level fields skipped silently (no "..." line)
+};
+
struct TxDocOptions {
/// The description of the txid field
std::string txid_field_doc{"The transaction id"};
/// Include wallet-related fields (e.g. ischange on outputs)
bool wallet{false};
- /// Treat this as an elided Result in the help
- std::optional<std::string> elision_description{};
+ /// Controls top-level field elision in the help
+ ElisionMode elision_mode{ElisionMode::None};
+ /// Summary text shown as "..." required for elision_mode == WithSummary
+ std::optional<std::string> elision_summary{};
+ /// Include prevout field
+ bool prevout{false};
+ /// Mark prevout field as optional (omitted when undo data unavailable)
+ bool prevout_optional{false};
+ /// Elide vin inner fields but keep vin array with prevout expanded.
+ std::optional<std::string> vin_inner_elision{};
};
/** Explain the UniValue "decoded" transaction object, may include extra fields if processed by wallet **/
std::vector<RPCResult> TxDoc(const TxDocOptions& opts = {});
diff --git a/src/rpc/util.cpp b/src/rpc/util.cpp
index dd207278..850acee8 100644
--- a/src/rpc/util.cpp
+++ b/src/rpc/util.cpp
@@ -1015,16 +1015,11 @@ void RPCResult::ToSections(Sections& sections, const OuterType outer_type, const
(this->m_description.empty() ? "" : " " + this->m_description);
};
- // Ensure at least one elision description exists, if there is any elision
+ // Ensure at least one visible field exists when elision is used
const auto elision_has_description{[](const std::vector<RPCResult>& inner) {
- const auto is_elided = [](const RPCResult& res) {
- return !std::holds_alternative<HelpElisionNone>(res.m_opts.print_elision);
- };
- const auto has_summary_text = [](const RPCResult& res) {
- const auto* text = std::get_if<std::string>(&res.m_opts.print_elision);
- return text && !text->empty();
- };
- return std::ranges::none_of(inner, is_elided) || std::ranges::any_of(inner, has_summary_text);
+ return std::ranges::any_of(inner, [](const auto& res) {
+ return !std::holds_alternative<HelpElisionSkip>(res.m_opts.print_elision);
+ });
}};
if (const auto* text = std::get_if<std::string>(&m_opts.print_elision)) {
Why this scored 18/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.