What changed, and why it matters
This change tightens how PSBT (Partially Signed Bitcoin Transaction) files are read. Previously, transaction-valued fields could contain extra bytes after the valid transaction data; now the parser rejects such trailing data. This prevents a malformed or crafted PSBT from sneaking in unused bytes that might confuse downstream tools or alter how the PSBT is interpreted.
Review whether other PSBT fields that deserialize wire messages (e.g., `readTxOut`) should also enforce full consumption for consistency. Consider adding regression tests with trailing-byte PSBTs. No immediate emergency action is indicated, but users parsing untrusted PSBTs should upgrade.
Security signals we found
Strict parsing of serialized transaction fields
Rejection of trailing/padding bytes in PSBT transaction values
Prevention of ambiguous or malleable PSBT parsing
Potential defense against crafted PSBTs that include hidden extra data
Evidence from the diff
The commit introduces a helper readTransaction in psbt/utils.go that deserializes a transaction and then calls assertFullyConsumed to ensure no bytes remain. It replaces two call sites: the global transaction in psbt/psbt.go (DeserializeNoWitness) and the non-witness UTXO transaction in psbt/partial_input.go (Deserialize). The change enforces BIP-174’s intent that transaction-valued fields contain exactly one network-serialized transaction, not a prefix with arbitrary trailing data.
Changed components
psbt/utils.gopsbt/psbt.gopsbt/partial_input.goInspect captured patch +26 / −6
diff --git a/psbt/partial_input.go b/psbt/partial_input.go
index 2e784be..8ac5d7d 100644
--- a/psbt/partial_input.go
+++ b/psbt/partial_input.go
@@ -90,9 +90,7 @@ func (pi *PInput) deserialize(r io.Reader) error {
if keyData != nil {
return ErrInvalidKeyData
}
- tx := wire.NewMsgTx(2)
-
- err := tx.Deserialize(bytes.NewReader(value))
+ tx, err := readTransaction(value, false)
if err != nil {
return err
}
diff --git a/psbt/psbt.go b/psbt/psbt.go
index 264f671..7548b52 100644
--- a/psbt/psbt.go
+++ b/psbt/psbt.go
@@ -225,11 +225,9 @@ func NewFromRawBytes(r io.Reader, b64 bool) (*Packet, error) {
if err != nil {
return nil, err
}
- msgTx := wire.NewMsgTx(2)
-
// BIP-0174 states: "The transaction must be in the old serialization
// format (without witnesses)."
- err = msgTx.DeserializeNoWitness(bytes.NewReader(value))
+ msgTx, err := readTransaction(value, true)
if err != nil {
return nil, err
}
diff --git a/psbt/utils.go b/psbt/utils.go
index 9596c48..0d99bfe 100644
--- a/psbt/utils.go
+++ b/psbt/utils.go
@@ -316,6 +316,30 @@ func readTxOut(txout []byte) (*wire.TxOut, error) {
return wire.NewTxOut(int64(valueSer), scriptPubKey), nil
}
+// readTransaction parses a transaction value and requires the full value to be
+// consumed. PSBT transaction-valued fields contain exactly one network
+// serialized transaction, not a transaction prefix with arbitrary trailing data.
+func readTransaction(txBytes []byte, noWitness bool) (*wire.MsgTx, error) {
+ tx := wire.NewMsgTx(2)
+ reader := bytes.NewReader(txBytes)
+
+ var err error
+ if noWitness {
+ err = tx.DeserializeNoWitness(reader)
+ } else {
+ err = tx.Deserialize(reader)
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ if err := assertFullyConsumed(reader); err != nil {
+ return nil, err
+ }
+
+ return tx, nil
+}
+
// SumUtxoInputValues tries to extract the sum of all inputs specified in the
// UTXO fields of the PSBT. An error is returned if an input is specified that
// does not contain any UTXO information.
Why this scored 51/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.