From 6853e69a05c7c253d1324ac616ccbe5ba4985668 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Tue, 11 Aug 2026 11:36:57 +0200 Subject: [PATCH] instantout: verify finalized MuSig2 witnesses Run script validation for every combined signature before accepting a finalized transaction, surfacing invalid witnesses immediately. --- instantout/instantout.go | 17 ++++++++++++++++ instantout/instantout_test.go | 37 +++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/instantout/instantout.go b/instantout/instantout.go index 40cbf12e..16f91d45 100644 --- a/instantout/instantout.go +++ b/instantout/instantout.go @@ -364,6 +364,9 @@ func (i *InstantOut) finalizeMusig2Transaction(ctx context.Context, "expected %d, got %d", len(inputs), len(serverSigs)) } + prevOutFetcher := inputs.GetPrevoutFetcher() + sigHashes := txscript.NewTxSigHashes(tx, prevOutFetcher) + for idx := range inputs { if musig2Sessions[idx] == nil { return nil, fmt.Errorf("MuSig2 session %d is nil", idx) @@ -382,6 +385,20 @@ func (i *InstantOut) finalizeMusig2Transaction(ctx context.Context, } tx.TxIn[idx].Witness = wire.TxWitness{finalSig} + + vm, err := txscript.NewEngine( + inputs[idx].PkScript, tx, idx, + txscript.StandardVerifyFlags, nil, sigHashes, + int64(inputs[idx].Value), prevOutFetcher, + ) + if err != nil { + return nil, fmt.Errorf("unable to verify final MuSig2 "+ + "signature for input %d: %w", idx, err) + } + if err := vm.Execute(); err != nil { + return nil, fmt.Errorf("invalid final MuSig2 signature "+ + "for input %d: %w", idx, err) + } } return tx, nil diff --git a/instantout/instantout_test.go b/instantout/instantout_test.go index 6b0e7cb5..f2ba3241 100644 --- a/instantout/instantout_test.go +++ b/instantout/instantout_test.go @@ -7,11 +7,22 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/lndclient" "github.com/lightninglabs/loop/instantout/reservation" "github.com/lightningnetwork/lnd/input" "github.com/stretchr/testify/require" ) +type invalidFinalSigSigner struct { + lndclient.SignerClient +} + +func (s *invalidFinalSigSigner) MuSig2CombineSig(context.Context, [32]byte, + [][]byte) (bool, []byte, error) { + + return true, make([]byte, 64), nil +} + // TestMuSig2VectorLengthValidation verifies that malformed server-controlled // vectors are rejected before they can be indexed. func TestMuSig2VectorLengthValidation(t *testing.T) { @@ -45,3 +56,29 @@ func TestMuSig2VectorLengthValidation(t *testing.T) { require.ErrorContains(t, err, "server signatures") }) } + +// TestFinalizeMuSig2TransactionVerifiesSignature verifies that a combined +// signature is validated locally before the transaction can be used as the +// instant-out safety net. +func TestFinalizeMuSig2TransactionVerifiesSignature(t *testing.T) { + _, pubKey := btcec.PrivKeyFromBytes([]byte{1}) + res := &reservation.Reservation{ + ClientPubkey: pubKey, + ServerPubkey: pubKey, + Value: btcutil.Amount(100_000), + Expiry: 200, + Outpoint: &wire.OutPoint{}, + } + instantOut := &InstantOut{ + Reservations: []*reservation.Reservation{res}, + } + tx := wire.NewMsgTx(2) + tx.AddTxIn(&wire.TxIn{PreviousOutPoint: *res.Outpoint}) + tx.AddTxOut(&wire.TxOut{Value: 90_000}) + + _, err := instantOut.finalizeMusig2Transaction( + context.Background(), &invalidFinalSigSigner{}, + []*input.MuSig2SessionInfo{{}}, tx, [][]byte{{1}}, + ) + require.ErrorContains(t, err, "invalid final MuSig2 signature") +}