What changed, and why it matters
This commit updates the Bitcoin Core 'decodepsbt' RPC command so it can display the newer PSBT version 2 format. It only changes what information is shown to the user; it does not change how transactions are validated, signed, or broadcast. There is no indication this is a security fix.
No security action required; treat as a normal feature/compatability update.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch extends src/rpc/rawtransaction.cpp’s decodepsbt output schema and population logic to support PSBTv2 fields. For PSBTv2 it omits the legacy ‘tx’ object and adds global fields (tx_version, fallback_locktime, input/output counts, modifiable flags) and per-input/output fields (previous_txid, previous_vout, sequence, locktimes, amount, scriptPubKey). The change is purely presentational RPC documentation/serialization.
Changed components
src/rpc/rawtransaction.cppdecodepsbt RPCInspect captured patch +62 / −6
diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp
index 7eb1c68c..dfabf073 100644
--- a/src/rpc/rawtransaction.cpp
+++ b/src/rpc/rawtransaction.cpp
@@ -845,6 +845,11 @@ const RPCResult& DecodePSBTInputs()
{
{RPCResult::Type::STR, "hash", "The hash and preimage that corresponds to it."},
}},
+ {RPCResult::Type::STR_HEX, "previous_txid", /*optional=*/true, "TXID of the transaction containing the output being spent by this input"},
+ {RPCResult::Type::NUM, "previous_vout", /*optional=*/true, "Index of the output being spent"},
+ {RPCResult::Type::NUM, "sequence", /*optional=*/true, "Sequence number for this input"},
+ {RPCResult::Type::NUM, "time_locktime", /*optional=*/true, "Time-based locktime required for this input"},
+ {RPCResult::Type::NUM, "height_locktime", /*optional=*/true, "Height-based locktime required for this input"},
{RPCResult::Type::STR_HEX, "taproot_key_path_sig", /*optional=*/ true, "hex-encoded signature for the Taproot key path spend"},
{RPCResult::Type::ARR, "taproot_script_path_sigs", /*optional=*/ true, "",
{
@@ -961,6 +966,10 @@ const RPCResult& DecodePSBTOutputs()
{RPCResult::Type::STR, "path", "The path"},
}},
}},
+ {RPCResult::Type::NUM, "amount", /* optional=*/ true, "The amount (nValue) for this output"},
+ {RPCResult::Type::OBJ, "script", /* optional=*/ true, "The output script (scriptPubKey) for this output",
+ {{RPCResult::Type::ELISION, "", "The layout is the same as the output of scriptPubKeys in decoderawtransaction."}},
+ },
{RPCResult::Type::STR_HEX, "taproot_internal_key", /*optional=*/ true, "The hex-encoded Taproot x-only internal key"},
{RPCResult::Type::ARR, "taproot_tree", /*optional=*/ true, "The tuples that make up the Taproot tree, in depth first search order",
{
@@ -1026,7 +1035,7 @@ static RPCMethod decodepsbt()
RPCResult{
RPCResult::Type::OBJ, "", "",
{
- {RPCResult::Type::OBJ, "tx", "The decoded network-serialized unsigned transaction.",
+ {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."})
},
{RPCResult::Type::ARR, "global_xpubs", "",
@@ -1038,7 +1047,14 @@ static RPCMethod decodepsbt()
{RPCResult::Type::STR, "path", "The path"},
}},
}},
- {RPCResult::Type::NUM, "psbt_version", "The PSBT version number. Not to be confused with the unsigned transaction version"},
+ {RPCResult::Type::NUM, "tx_version", /* optional */ true, "The version number of the unsigned transaction. Not to be confused with PSBT version"},
+ {RPCResult::Type::NUM, "fallback_locktime", /* optional */ true, "The locktime to fallback to if no inputs specify a required locktime."},
+ {RPCResult::Type::NUM, "input_count", /* optional */ true, "The number of inputs in this psbt"},
+ {RPCResult::Type::NUM, "output_count", /* optional */ true, "The number of outputs in this psbt."},
+ {RPCResult::Type::BOOL, "inputs_modifiable", /* optional */ true, "Whether inputs can be modified"},
+ {RPCResult::Type::BOOL, "outputs_modifiable", /* optional */ true, "Whether outputs can be modified"},
+ {RPCResult::Type::BOOL, "has_sighash_single", /* optional */ true, "Whether this PSBT has SIGHASH_SINGLE inputs"},
+ {RPCResult::Type::NUM, "psbt_version", /* optional */ true, "The PSBT version number. Not to be confused with the unsigned transaction version"},
{RPCResult::Type::ARR, "proprietary", "The global proprietary map",
{
{RPCResult::Type::OBJ, "", "",
@@ -1072,10 +1088,12 @@ static RPCMethod decodepsbt()
UniValue result(UniValue::VOBJ);
- // Add the decoded tx
- UniValue tx_univ(UniValue::VOBJ);
- TxToUniv(CTransaction(*CHECK_NONFATAL(psbtx.GetUnsignedTx())), /*block_hash=*/uint256(), /*entry=*/tx_univ, /*include_hex=*/false);
- result.pushKV("tx", std::move(tx_univ));
+ if (psbtx.GetVersion() < 2) {
+ // Add the decoded tx
+ UniValue tx_univ(UniValue::VOBJ);
+ TxToUniv(CTransaction(*CHECK_NONFATAL(psbtx.GetUnsignedTx())), /*block_hash=*/uint256(), /*entry=*/tx_univ, /*include_hex=*/false);
+ result.pushKV("tx", std::move(tx_univ));
+ }
// Add the global xpubs
UniValue global_xpubs(UniValue::VARR);
@@ -1094,6 +1112,21 @@ static RPCMethod decodepsbt()
}
result.pushKV("global_xpubs", std::move(global_xpubs));
+ // Add PSBTv2 stuff
+ if (psbtx.GetVersion() >= 2) {
+ result.pushKV("tx_version", psbtx.tx_version);
+ if (psbtx.fallback_locktime.has_value()) {
+ result.pushKV("fallback_locktime", static_cast<uint64_t>(*psbtx.fallback_locktime));
+ }
+ result.pushKV("input_count", (uint64_t)psbtx.inputs.size());
+ result.pushKV("output_count", (uint64_t)psbtx.outputs.size());
+ if (psbtx.m_tx_modifiable.has_value()) {
+ result.pushKV("inputs_modifiable", psbtx.m_tx_modifiable->test(0));
+ result.pushKV("outputs_modifiable", psbtx.m_tx_modifiable->test(1));
+ result.pushKV("has_sighash_single", psbtx.m_tx_modifiable->test(2));
+ }
+ }
+
// PSBT version
result.pushKV("psbt_version", psbtx.GetVersion());
@@ -1251,6 +1284,21 @@ static RPCMethod decodepsbt()
in.pushKV("hash256_preimages", std::move(hash256_preimages));
}
+ // PSBTv2
+ if (psbtx.GetVersion() >= 2) {
+ in.pushKV("previous_txid", input.prev_txid.GetHex());
+ in.pushKV("previous_vout", static_cast<uint64_t>(input.prev_out));
+ if (input.sequence.has_value()) {
+ in.pushKV("sequence", static_cast<uint64_t>(*input.sequence));
+ }
+ if (input.time_locktime.has_value()) {
+ in.pushKV("time_locktime", static_cast<uint64_t>(*input.time_locktime));
+ }
+ if (input.height_locktime.has_value()) {
+ in.pushKV("height_locktime", static_cast<uint64_t>(*input.height_locktime));
+ }
+ }
+
// Taproot key path signature
if (!input.m_tap_key_sig.empty()) {
in.pushKV("taproot_key_path_sig", HexStr(input.m_tap_key_sig));
@@ -1421,6 +1469,14 @@ static RPCMethod decodepsbt()
out.pushKV("bip32_derivs", std::move(keypaths));
}
+ // PSBTv2 stuff
+ if (psbtx.GetVersion() >= 2) {
+ out.pushKV("amount", ValueFromAmount(output.amount));
+ UniValue spk(UniValue::VOBJ);
+ ScriptToUniv(output.script, spk, /*include_hex=*/true, /*include_address=*/true);
+ out.pushKV("script", spk);
+ }
+
// Taproot internal key
if (!output.m_tap_internal_key.IsNull()) {
out.pushKV("taproot_internal_key", HexStr(output.m_tap_internal_key));
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.