diff --git a/psbt/finalizer.go b/psbt/finalizer.go index fa1d8f2b..a2a1e2d2 100644 --- a/psbt/finalizer.go +++ b/psbt/finalizer.go @@ -55,6 +55,10 @@ func isFinalizableWitnessInput(pInput *PInput) bool { // For each of the script spend signatures we need a // corresponding tap script leaf with the control block. for _, sig := range pInput.TaprootScriptSpendSig { + if sig == nil { + return false + } + _, err := FindLeafScript(pInput, sig.LeafHash) if err != nil { return false @@ -516,6 +520,13 @@ func finalizeTaprootInput(p *Packet, inIndex int) error { pInput = &p.Inputs[inIndex] ) + for idx, scriptSpendSig := range pInput.TaprootScriptSpendSig { + if scriptSpendSig == nil { + return fmt.Errorf("nil taproot script spend signature "+ + "at index %d: %w", idx, ErrInvalidPsbtFormat) + } + } + // What spend path did we take? switch { // Key spend path. @@ -547,8 +558,8 @@ func finalizeTaprootInput(p *Packet, inIndex int) error { targetLeafHash := pInput.TaprootScriptSpendSig[0].LeafHash leafScript, err := FindLeafScript(pInput, targetLeafHash) if err != nil { - return fmt.Errorf("control block for script spend " + - "signature not found") + return fmt.Errorf("control block for script spend "+ + "signature not found: %w", err) } // The witness stack will contain all signatures, followed by diff --git a/psbt/partial_input.go b/psbt/partial_input.go index 9c1c99c6..0dc57959 100644 --- a/psbt/partial_input.go +++ b/psbt/partial_input.go @@ -495,6 +495,13 @@ func (pi *PInput) serialize(w io.Writer) error { } } + for idx, scriptSpend := range pi.TaprootScriptSpendSig { + if scriptSpend == nil { + return fmt.Errorf("nil taproot script spend "+ + "signature at index %d: %w", idx, + ErrInvalidPsbtFormat) + } + } sort.Slice(pi.TaprootScriptSpendSig, func(i, j int) bool { return pi.TaprootScriptSpendSig[i].SortBefore( pi.TaprootScriptSpendSig[j], @@ -518,7 +525,8 @@ 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) + return fmt.Errorf("nil taproot leaf script at "+ + "index %d: %w", idx, ErrInvalidPsbtFormat) } } sort.Slice(pi.TaprootLeafScript, func(i, j int) bool { @@ -538,6 +546,12 @@ func (pi *PInput) serialize(w io.Writer) error { } } + for idx, derivation := range pi.TaprootBip32Derivation { + if derivation == nil { + return fmt.Errorf("nil taproot BIP32 derivation at "+ + "index %d: %w", idx, ErrInvalidPsbtFormat) + } + } sort.Slice(pi.TaprootBip32Derivation, func(i, j int) bool { return pi.TaprootBip32Derivation[i].SortBefore( pi.TaprootBip32Derivation[j], diff --git a/psbt/partial_output.go b/psbt/partial_output.go index 94b5d33f..93233e0e 100644 --- a/psbt/partial_output.go +++ b/psbt/partial_output.go @@ -2,6 +2,7 @@ package psbt import ( "bytes" + "fmt" "io" "sort" @@ -225,6 +226,12 @@ func (po *POutput) serialize(w io.Writer) error { } } + for idx, derivation := range po.TaprootBip32Derivation { + if derivation == nil { + return fmt.Errorf("nil taproot BIP32 derivation at "+ + "index %d: %w", idx, ErrInvalidPsbtFormat) + } + } sort.Slice(po.TaprootBip32Derivation, func(i, j int) bool { return po.TaprootBip32Derivation[i].SortBefore( po.TaprootBip32Derivation[j], diff --git a/psbt/psbt_test.go b/psbt/psbt_test.go index c2907dbc..1f36a97c 100644 --- a/psbt/psbt_test.go +++ b/psbt/psbt_test.go @@ -1338,24 +1338,138 @@ 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, +func TestB64EncodeRejectsNilTaprootFields(t *testing.T) { + testCases := []struct { + name string + expectedErr string + setNil func(*Packet) + }{ + { + name: "input script spend signature", + expectedErr: "nil taproot script spend signature at index 0", + setNil: func(packet *Packet) { + packet.Inputs[0].TaprootScriptSpendSig = + []*TaprootScriptSpendSig{nil} + }, }, - }) - tx.AddTxOut(wire.NewTxOut(1, []byte{txscript.OP_TRUE})) + { + name: "input leaf script", + expectedErr: "nil taproot leaf script at index 0", + setNil: func(packet *Packet) { + packet.Inputs[0].TaprootLeafScript = + []*TaprootTapLeafScript{nil} + }, + }, + { + name: "input BIP32 derivation", + expectedErr: "nil taproot BIP32 derivation at index 0", + setNil: func(packet *Packet) { + packet.Inputs[0].TaprootBip32Derivation = + []*TaprootBip32Derivation{nil} + }, + }, + { + name: "output BIP32 derivation", + expectedErr: "nil taproot BIP32 derivation at index 0", + setNil: func(packet *Packet) { + packet.Outputs[0].TaprootBip32Derivation = + []*TaprootBip32Derivation{nil} + }, + }, + } - packet, err := NewFromUnsignedTx(tx) - require.NoError(t, err) + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + tx := wire.NewMsgTx(2) + tx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: wire.OutPoint{}, + }) + tx.AddTxOut(wire.NewTxOut( + 1, []byte{txscript.OP_TRUE}, + )) - packet.Inputs[0].TaprootLeafScript = []*TaprootTapLeafScript{nil} - _, err = packet.B64Encode() + packet, err := NewFromUnsignedTx(tx) + require.NoError(t, err) + testCase.setNil(packet) + + _, err = packet.B64Encode() + require.ErrorIs(t, err, ErrInvalidPsbtFormat) + require.ErrorContains(t, err, testCase.expectedErr) + }) + } +} + +func TestFindLeafScriptRejectsNilLeaf(t *testing.T) { + _, err := FindLeafScript(nil, make([]byte, chainhash.HashSize)) + require.ErrorIs(t, err, ErrInvalidPsbtFormat) + require.ErrorContains(t, err, "nil PSBT input") + + input := &PInput{ + TaprootLeafScript: []*TaprootTapLeafScript{nil}, + } + + _, err = FindLeafScript(input, make([]byte, chainhash.HashSize)) + require.ErrorIs(t, err, ErrInvalidPsbtFormat) require.ErrorContains(t, err, "nil taproot leaf script at index 0") } +func TestFinalizeRejectsNilTaprootFields(t *testing.T) { + newPacket := func() *Packet { + tx := wire.NewMsgTx(2) + tx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: wire.OutPoint{}, + }) + tx.AddTxOut(wire.NewTxOut( + 1, []byte{txscript.OP_TRUE}, + )) + + packet, err := NewFromUnsignedTx(tx) + require.NoError(t, err) + packet.Inputs[0].WitnessUtxo = wire.NewTxOut( + 1, append( + []byte{txscript.OP_1, txscript.OP_DATA_32}, + make([]byte, 32)..., + ), + ) + + return packet + } + + t.Run("script spend signature", func(t *testing.T) { + packet := newPacket() + packet.Inputs[0].TaprootScriptSpendSig = + []*TaprootScriptSpendSig{nil} + + finalized, err := MaybeFinalize(packet, 0) + require.False(t, finalized) + require.ErrorIs(t, err, ErrNotFinalizable) + + err = Finalize(packet, 0) + require.ErrorIs(t, err, ErrInvalidPsbtFormat) + require.ErrorContains( + t, err, "nil taproot script spend signature at index 0", + ) + }) + + t.Run("leaf script", func(t *testing.T) { + packet := newPacket() + packet.Inputs[0].TaprootScriptSpendSig = + []*TaprootScriptSpendSig{{ + LeafHash: make([]byte, chainhash.HashSize), + }} + packet.Inputs[0].TaprootLeafScript = + []*TaprootTapLeafScript{nil} + + finalized, err := MaybeFinalize(packet, 0) + require.False(t, finalized) + require.ErrorIs(t, err, ErrNotFinalizable) + + err = Finalize(packet, 0) + require.ErrorIs(t, err, ErrInvalidPsbtFormat) + 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 diff --git a/psbt/utils.go b/psbt/utils.go index 2c880e2b..6053bca5 100644 --- a/psbt/utils.go +++ b/psbt/utils.go @@ -464,7 +464,16 @@ func NewFromSignedTx(tx *wire.MsgTx) (*Packet, [][]byte, func FindLeafScript(pInput *PInput, targetLeafHash []byte) (*TaprootTapLeafScript, error) { - for _, leaf := range pInput.TaprootLeafScript { + if pInput == nil { + return nil, fmt.Errorf("nil PSBT input: %w", ErrInvalidPsbtFormat) + } + + for idx, leaf := range pInput.TaprootLeafScript { + if leaf == nil { + return nil, fmt.Errorf("nil taproot leaf script at index "+ + "%d: %w", idx, ErrInvalidPsbtFormat) + } + leafHash := txscript.TapLeaf{ LeafVersion: leaf.LeafVersion, Script: leaf.Script,