From fadf901fd4b4d95bd0dd046b8d44a1154c5ea9a8 Mon Sep 17 00:00:00 2001 From: MarcoFalke <*~=`'#}+{/-|&$^_@721217.xyz> Date: Wed, 11 Mar 2026 11:55:41 +0100 Subject: [PATCH] rpc: Run type check on decodepsbt result For RPCResults, the type may be ELISION, which is confusing and brittle: * The elision should only affect the help output, not the type. * The type should be the real type, so that type checks can be run on it. Fix this issue by introducing a new print_elision option and using it in decodepsbt. This change will ensure that RPCResult::MatchesType is properly run. Also, this clarifies the RPC output minimally: ```diff --- a/decodepsbt +++ b/decodepsbt @@ -35,7 +35,7 @@ Result: "inputs" : [ (json array) { (json object) "non_witness_utxo" : { (json object, optional) Decoded network transaction for non-witness UTXOs - ... + ... The layout is the same as the output of decoderawtransaction. }, "witness_utxo" : { (json object, optional) Transaction output for witness UTXOs "amount" : n, (numeric) The value in BTC ``` --- src/rpc/rawtransaction.cpp | 10 ++++------ src/rpc/rawtransaction_util.cpp | 20 +++++++++++--------- src/rpc/rawtransaction_util.h | 2 ++ src/rpc/util.cpp | 17 ++++++++++++++++- src/rpc/util.h | 9 +++++++++ 5 files changed, 42 insertions(+), 16 deletions(-) diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index 3632cc49ba4..7a202ccfcc3 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -781,9 +781,8 @@ const RPCResult decodepsbt_inputs{ {RPCResult::Type::OBJ, "", "", { {RPCResult::Type::OBJ, "non_witness_utxo", /*optional=*/true, "Decoded network transaction for non-witness UTXOs", - { - {RPCResult::Type::ELISION, "",""}, - }}, + TxDoc({.elision_description="The layout is the same as the output of decoderawtransaction."}) + }, {RPCResult::Type::OBJ, "witness_utxo", /*optional=*/true, "Transaction output for witness UTXOs", { {RPCResult::Type::NUM, "amount", "The value in " + CURRENCY_UNIT}, @@ -1023,9 +1022,8 @@ static RPCHelpMan decodepsbt() RPCResult::Type::OBJ, "", "", { {RPCResult::Type::OBJ, "tx", "The decoded network-serialized unsigned transaction.", - { - {RPCResult::Type::ELISION, "", "The layout is the same as the output of decoderawtransaction."}, - }}, + TxDoc({.elision_description="The layout is the same as the output of decoderawtransaction."}) + }, {RPCResult::Type::ARR, "global_xpubs", "", { {RPCResult::Type::OBJ, "", "", diff --git a/src/rpc/rawtransaction_util.cpp b/src/rpc/rawtransaction_util.cpp index 89fc1e271ca..d3a1b875807 100644 --- a/src/rpc/rawtransaction_util.cpp +++ b/src/rpc/rawtransaction_util.cpp @@ -346,14 +346,16 @@ void SignTransactionResultToJSON(CMutableTransaction& mtx, bool complete, const std::vector TxDoc(const TxDocOptions& opts) { + std::optional maybe_skip{}; + if (opts.elision_description) maybe_skip.emplace(); return { - {RPCResult::Type::STR_HEX, "txid", opts.txid_field_doc}, - {RPCResult::Type::STR_HEX, "hash", "The transaction hash (differs from txid for witness transactions)"}, - {RPCResult::Type::NUM, "size", "The serialized transaction size"}, - {RPCResult::Type::NUM, "vsize", "The virtual transaction size (differs from size for witness transactions)"}, - {RPCResult::Type::NUM, "weight", "The transaction's weight (between vsize*4-3 and vsize*4)"}, - {RPCResult::Type::NUM, "version", "The version"}, - {RPCResult::Type::NUM_TIME, "locktime", "The lock time"}, + {RPCResult::Type::STR_HEX, "txid", opts.txid_field_doc, {}, {.print_elision=opts.elision_description}}, + {RPCResult::Type::STR_HEX, "hash", "The transaction hash (differs from txid for witness transactions)", {}, {.print_elision=maybe_skip}}, + {RPCResult::Type::NUM, "size", "The serialized transaction size", {}, {.print_elision=maybe_skip}}, + {RPCResult::Type::NUM, "vsize", "The virtual transaction size (differs from size for witness transactions)", {}, {.print_elision=maybe_skip}}, + {RPCResult::Type::NUM, "weight", "The transaction's weight (between vsize*4-3 and vsize*4)", {}, {.print_elision=maybe_skip}}, + {RPCResult::Type::NUM, "version", "The version", {}, {.print_elision=maybe_skip}}, + {RPCResult::Type::NUM_TIME, "locktime", "The lock time", {}, {.print_elision=maybe_skip}}, {RPCResult::Type::ARR, "vin", "", { {RPCResult::Type::OBJ, "", "", @@ -372,7 +374,7 @@ std::vector TxDoc(const TxDocOptions& opts) }}, {RPCResult::Type::NUM, "sequence", "The script sequence number"}, }}, - }}, + }, {.print_elision=maybe_skip}}, {RPCResult::Type::ARR, "vout", "", { {RPCResult::Type::OBJ, "", "", Cat( @@ -386,6 +388,6 @@ std::vector TxDoc(const TxDocOptions& opts) std::vector{} ) }, - }}, + }, {.print_elision=maybe_skip}}, }; } diff --git a/src/rpc/rawtransaction_util.h b/src/rpc/rawtransaction_util.h index ed1efd0b6a5..5a1bc6047c7 100644 --- a/src/rpc/rawtransaction_util.h +++ b/src/rpc/rawtransaction_util.h @@ -61,6 +61,8 @@ struct TxDocOptions { 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 elision_description{}; }; /** Explain the UniValue "decoded" transaction object, may include extra fields if processed by wallet **/ std::vector TxDoc(const TxDocOptions& opts = {}); diff --git a/src/rpc/util.cpp b/src/rpc/util.cpp index 75cfa8d3af6..cee8a4295ba 100644 --- a/src/rpc/util.cpp +++ b/src/rpc/util.cpp @@ -1015,9 +1015,22 @@ 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 + const auto elision_has_description{[](const std::vector& inner) { + return std::ranges::none_of(inner, [](const auto& res) { return res.m_opts.print_elision.has_value(); }) || + std::ranges::any_of(inner, [](const auto& res) { return res.m_opts.print_elision.has_value() && !res.m_opts.print_elision->empty(); }); + }}; + + if (m_opts.print_elision) { + if (!m_opts.print_elision->empty()) { + sections.PushSection({indent + "..." + maybe_separator, *m_opts.print_elision}); + } + return; + } + switch (m_type) { case Type::ELISION: { - // If the inner result is empty, use three dots for elision + // Deprecated alias of m_opts.print_elision sections.PushSection({indent + "..." + maybe_separator, m_description}); return; } @@ -1059,6 +1072,7 @@ void RPCResult::ToSections(Sections& sections, const OuterType outer_type, const i.ToSections(sections, OuterType::ARR, current_indent + 2); } CHECK_NONFATAL(!m_inner.empty()); + CHECK_NONFATAL(elision_has_description(m_inner)); if (m_type == Type::ARR && m_inner.back().m_type != Type::ELISION) { sections.PushSection({indent_next + "...", ""}); } else { @@ -1074,6 +1088,7 @@ void RPCResult::ToSections(Sections& sections, const OuterType outer_type, const sections.PushSection({indent + maybe_key + "{}", Description("empty JSON object")}); return; } + CHECK_NONFATAL(elision_has_description(m_inner)); sections.PushSection({indent + maybe_key + "{", Description("json object")}); for (const auto& i : m_inner) { i.ToSections(sections, OuterType::OBJ, current_indent + 2); diff --git a/src/rpc/util.h b/src/rpc/util.h index a3f95964684..fda71072f3b 100644 --- a/src/rpc/util.h +++ b/src/rpc/util.h @@ -294,6 +294,15 @@ struct RPCArg { struct RPCResultOptions { bool skip_type_check{false}; + /// Whether to treat this as elided in the human-readable description, and + /// possibly supply a description for the elision. Normally, there will be + /// one string on any of the elided results, for example `Same output as + /// verbosity = 1`, and all other elided strings will be empty. + /// + /// - If nullopt: normal display. + /// - If empty string: suppress from help. + /// - If non-empty: show "..." with this description. + std::optional print_elision{std::nullopt}; }; // NOLINTNEXTLINE(misc-no-recursion) struct RPCResult {