From b3776e92f45ff1c9cb61b7f2a3261e47eb3c7066 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Tue, 11 Aug 2026 11:36:27 +0200 Subject: [PATCH] instantout: validate MuSig2 response dimensions Check nonce, signature, session, and transaction input counts before indexing signing vectors, returning clear errors for incomplete data. --- instantout/instantout.go | 38 ++++++++++++++++++++++++++++ instantout/instantout_test.go | 47 +++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 instantout/instantout_test.go diff --git a/instantout/instantout.go b/instantout/instantout.go index f8c89eb0..40cbf12e 100644 --- a/instantout/instantout.go +++ b/instantout/instantout.go @@ -263,12 +263,31 @@ func (i *InstantOut) signMusig2Tx(ctx context.Context, if err != nil { return nil, err } + if tx == nil { + return nil, errors.New("transaction is nil") + } + if len(tx.TxIn) != len(inputs) { + return nil, fmt.Errorf("invalid number of transaction inputs: "+ + "expected %d, got %d", len(inputs), len(tx.TxIn)) + } + if len(musig2sessions) != len(inputs) { + return nil, fmt.Errorf("invalid number of MuSig2 sessions: "+ + "expected %d, got %d", len(inputs), len(musig2sessions)) + } + if len(counterPartyNonces) != len(inputs) { + return nil, fmt.Errorf("invalid number of server nonces: "+ + "expected %d, got %d", len(inputs), len(counterPartyNonces)) + } prevOutFetcher := inputs.GetPrevoutFetcher() sigHashes := txscript.NewTxSigHashes(tx, prevOutFetcher) sigs := make([][]byte, len(inputs)) for idx, reservation := range inputs { + if musig2sessions[idx] == nil { + return nil, fmt.Errorf("MuSig2 session %d is nil", idx) + } + if !reflect.DeepEqual(tx.TxIn[idx].PreviousOutPoint, reservation.Outpoint) { @@ -329,8 +348,27 @@ func (i *InstantOut) finalizeMusig2Transaction(ctx context.Context, if err != nil { return nil, err } + if tx == nil { + return nil, errors.New("transaction is nil") + } + if len(tx.TxIn) != len(inputs) { + return nil, fmt.Errorf("invalid number of transaction inputs: "+ + "expected %d, got %d", len(inputs), len(tx.TxIn)) + } + if len(musig2Sessions) != len(inputs) { + return nil, fmt.Errorf("invalid number of MuSig2 sessions: "+ + "expected %d, got %d", len(inputs), len(musig2Sessions)) + } + if len(serverSigs) != len(inputs) { + return nil, fmt.Errorf("invalid number of server signatures: "+ + "expected %d, got %d", len(inputs), len(serverSigs)) + } for idx := range inputs { + if musig2Sessions[idx] == nil { + return nil, fmt.Errorf("MuSig2 session %d is nil", idx) + } + haveAllSigs, finalSig, err := signer.MuSig2CombineSig( ctx, musig2Sessions[idx].SessionID, [][]byte{serverSigs[idx]}, diff --git a/instantout/instantout_test.go b/instantout/instantout_test.go new file mode 100644 index 00000000..6b0e7cb5 --- /dev/null +++ b/instantout/instantout_test.go @@ -0,0 +1,47 @@ +package instantout + +import ( + "context" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/loop/instantout/reservation" + "github.com/lightningnetwork/lnd/input" + "github.com/stretchr/testify/require" +) + +// TestMuSig2VectorLengthValidation verifies that malformed server-controlled +// vectors are rejected before they can be indexed. +func TestMuSig2VectorLengthValidation(t *testing.T) { + _, pubKey := btcec.PrivKeyFromBytes([]byte{1}) + instantOut := &InstantOut{ + Reservations: []*reservation.Reservation{ + { + ClientPubkey: pubKey, + ServerPubkey: pubKey, + Value: btcutil.Amount(100_000), + Expiry: 200, + Outpoint: &wire.OutPoint{}, + }, + }, + } + tx := wire.NewMsgTx(2) + tx.AddTxIn(&wire.TxIn{}) + sessions := []*input.MuSig2SessionInfo{{}} + + require.NotPanics(t, func() { + _, err := instantOut.signMusig2Tx( + context.Background(), nil, tx, sessions, nil, + ) + require.ErrorContains(t, err, "server nonces") + }) + + require.NotPanics(t, func() { + _, err := instantOut.finalizeMusig2Transaction( + context.Background(), nil, sessions, tx, nil, + ) + require.ErrorContains(t, err, "server signatures") + }) +}