Merge bitcoin/bitcoin#36113: psbt: fix rendering for invalid long sighash type field
What changed, and why it matters
This change fixes a display bug in Bitcoin Core's PSBT decoder. When a PSBT contains an invalid 32-bit sighash type value, the old code silently truncated it to one byte and then showed a friendly name for that truncated value, which could mislead a user into thinking an invalid signature type was valid. The fix makes the decoder return an empty string for any value that is not a single valid byte, and adds a test. It is not a security vulnerability that can steal funds or crash nodes.
No urgent action. Treat as a routine bug fix. Users relying on decodepsbt should note that invalid sighash type fields now render as empty strings instead of a misleading name.
Security signals we found
Informational UI/rendering bug in RPC output
Potential user confusion from incorrect sighash type label
No consensus, network, or wallet enforcement change
No memory safety, crash, or remote-code-execution issue
Evidence from the diff
SighashToStr previously took an unsigned char and was called from decodepsbt with an explicit cast from the 32-bit PSBT sighash_type field. The cast truncated values > 0xff, so a PSBT with sighash_type 0x101 would render as ‘ALL’ (0x01). The patch widens the parameter to int32_t, rejects values outside 0..0xff, and only then maps the lower byte. A functional test verifies that 0x101 decodes to an empty sighash string.
Changed components
src/core_io.cppsrc/core_io.hsrc/rpc/rawtransaction.cpptest/functional/rpc_psbt.pyInspect captured patch +18 / −4
### src/core_io.cpp
@@ -338,9 +338,13 @@ const std::map<unsigned char, std::string> mapSigHashTypes = {
{static_cast<unsigned char>(SIGHASH_SINGLE|SIGHASH_ANYONECANPAY), std::string("SINGLE|ANYONECANPAY")},
};
-std::string SighashToStr(unsigned char sighash_type)
+std::string SighashToStr(int32_t sighash_type)
{
- const auto& it = mapSigHashTypes.find(sighash_type);
+ // Signatures encode the sighash type in a single byte, but the PSBT field
+ // for it is a 32 bit unsigned integer in BIP 174 (signed in PSBTInput)
+ if (sighash_type < 0 || sighash_type > 0xff) return "";
+ const uint8_t sighash_byte(sighash_type);
+ const auto& it = mapSigHashTypes.find(sighash_byte);
if (it == mapSigHashTypes.end()) return "";
return it->second;
}
### src/core_io.h
@@ -8,6 +8,7 @@
#include <consensus/amount.h>
#include <util/result.h>
+#include <cstdint>
#include <functional>
#include <string>
@@ -42,7 +43,7 @@ bool DecodeHexBlockHeader(CBlockHeader&, const std::string& hex_header);
UniValue ValueFromAmount(CAmount amount);
std::string FormatScript(const CScript& script);
std::string EncodeHexTx(const CTransaction& tx);
-std::string SighashToStr(unsigned char sighash_type);
+std::string SighashToStr(int32_t sighash_type);
void ScriptToUniv(const CScript& script, UniValue& out, bool include_hex = true, bool include_address = false, const SigningProvider* provider = nullptr);
void TxToUniv(const CTransaction& tx, const uint256& block_hash, UniValue& entry, bool include_hex = true, const CTxUndo* txundo = nullptr, TxVerbosity verbosity = TxVerbosity::SHOW_DETAILS, std::function<bool(const CTxOut&)> is_change_func = {});
### src/rpc/rawtransaction.cpp
@@ -1224,7 +1224,7 @@ static RPCMethod decodepsbt()
// Sighash
if (input.sighash_type != std::nullopt) {
- in.pushKV("sighash", SighashToStr((unsigned char)*input.sighash_type));
+ in.pushKV("sighash", SighashToStr(*input.sighash_type));
}
// Redeem script and witness script
### test/functional/rpc_psbt.py
@@ -557,6 +557,14 @@ def test_sighash_adding(self):
wallet.unloadwallet()
+ def test_decodepsbt_long_sighash_type(self):
+ self.log.info("Test that decodepsbt rejects invalid trailing bytes in the sighash type field")
+ node = self.nodes[0]
+ psbt = PSBT.from_base64(node.createpsbt([{"txid": "00" * 32, "vout": 0}], [{"data": "00"}]))
+ # The first byte of this sighash type is ALL, but the type itself is not
+ psbt.i[0].map[PSBT_IN_SIGHASH_TYPE] = (0x101).to_bytes(4, "little")
+ assert_equal(node.decodepsbt(psbt.to_base64())["inputs"][0]["sighash"], "")
+
def assert_change_type(self, psbtx, expected_type):
"""Assert that the given PSBT has a change output with the given type."""
@@ -1619,6 +1627,7 @@ def global_xpub_key(extended_pubkey):
if not self.options.usecli:
self.test_sighash_mismatch()
self.test_sighash_adding()
+ self.test_decodepsbt_long_sighash_type()
self.test_psbt_named_parameter_handling()
self.test_psbt_roundtrip()
self.test_psbt_version()Why this scored 24/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.