What changed, and why it matters
This commit adds a safety check in btcd's PSBT (Partially Signed Bitcoin Transaction) handling code. Previously, if a taproot leaf script entry was accidentally set to nil (empty/missing), the code could crash with a panic when trying to sort or encode the PSBT. The fix now rejects nil entries with a clear error message instead of crashing. This is a defensive hardening change rather than a remote exploit, but it prevents a denial-of-service-style crash for applications that process untrusted PSBTs.
Review whether nil checks are also needed in deserialization paths and other Taproot-related PSBT fields. Consider backporting if the PSBT package is used in production services that process external PSBTs. No immediate emergency response is warranted.
Security signals we found
nil-pointer dereference prevention
input validation hardening
PSBT serialization safety
taproot leaf script handling
denial-of-service mitigation
Evidence from the diff
In psbt/partial_input.go’s PInput.serialize(), a new loop checks each element of pi.TaprootLeafScript for nil before calling sort.Slice and subsequent encoding logic. If a nil element is found, it returns fmt.Errorf(“nil taproot leaf script at index %d”, idx). A test in psbt/psbt_test.go verifies that B64Encode() returns this error when a nil TaprootTapLeafScript is present. The change prevents nil-pointer dereference panics during serialization of malformed PSBT inputs.
Changed components
btcd/psbt/partial_input.gobtcd/psbt/psbt_test.goPInput.serialize()B64Encode()Inspect captured patch +24 / −0
diff --git a/psbt/partial_input.go b/psbt/partial_input.go
index 2e784be..9c1c99c 100644
--- a/psbt/partial_input.go
+++ b/psbt/partial_input.go
@@ -3,6 +3,7 @@ package psbt
import (
"bytes"
"encoding/binary"
+ "fmt"
"io"
"sort"
@@ -515,6 +516,11 @@ func (pi *PInput) serialize(w io.Writer) error {
}
}
+ for idx, leafScript := range pi.TaprootLeafScript {
+ if leafScript == nil {
+ return fmt.Errorf("nil taproot leaf script at index %d", idx)
+ }
+ }
sort.Slice(pi.TaprootLeafScript, func(i, j int) bool {
return pi.TaprootLeafScript[i].SortBefore(
pi.TaprootLeafScript[j],
diff --git a/psbt/psbt_test.go b/psbt/psbt_test.go
index 8b8b3a6..c2907db 100644
--- a/psbt/psbt_test.go
+++ b/psbt/psbt_test.go
@@ -1338,6 +1338,24 @@ func TestFromUnsigned(t *testing.T) {
}
}
+func TestB64EncodeRejectsNilTaprootLeafScript(t *testing.T) {
+ tx := wire.NewMsgTx(2)
+ tx.AddTxIn(&wire.TxIn{
+ PreviousOutPoint: wire.OutPoint{
+ Hash: chainhash.Hash{},
+ Index: 0,
+ },
+ })
+ tx.AddTxOut(wire.NewTxOut(1, []byte{txscript.OP_TRUE}))
+
+ packet, err := NewFromUnsignedTx(tx)
+ require.NoError(t, err)
+
+ packet.Inputs[0].TaprootLeafScript = []*TaprootTapLeafScript{nil}
+ _, err = packet.B64Encode()
+ require.ErrorContains(t, err, "nil taproot leaf script at index 0")
+}
+
func TestNonWitnessToWitness(t *testing.T) {
// We'll start with a PSBT produced by Core for which
// the first input is signed and we'll provided the signatures for
Why this scored 36/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.