instantout: close unfinished MuSig2 sessions

Clean up abandoned signing sessions on error paths while leaving
completed sessions to lnd.
This commit is contained in:
Slyghtning 2026-08-11 11:37:46 +02:00
parent 6853e69a05
commit b689e361c1
No known key found for this signature in database
GPG key ID: F82D456EA023C9BF
3 changed files with 88 additions and 2 deletions

View file

@ -293,6 +293,15 @@ func (f *FSM) BuildHTLCAction(ctx context.Context,
}
f.htlcMusig2Sessions = htlcSessions
defer func() {
err := cleanupMuSig2Sessions(
ctx, f.cfg.Signer, f.htlcMusig2Sessions,
)
if err != nil {
f.Errorf("unable to clean up HTLC MuSig2 sessions: %v", err)
}
f.htlcMusig2Sessions = nil
}()
// Send the server the client nonces.
htlcInitRes, err := f.cfg.InstantOutClient.InitHtlcSig(
@ -382,6 +391,15 @@ func (f *FSM) PushPreimageAction(ctx context.Context,
}
f.sweeplessSweepSessions = coopSessions
defer func() {
err := cleanupMuSig2Sessions(
ctx, f.cfg.Signer, f.sweeplessSweepSessions,
)
if err != nil {
f.Errorf("unable to clean up sweep MuSig2 sessions: %v", err)
}
f.sweeplessSweepSessions = nil
}()
// Get the feerate for the coop sweep.
feeRate, err := f.cfg.Wallet.EstimateFeeRate(ctx, normalConfTarget)

View file

@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"reflect"
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
@ -25,6 +26,8 @@ import (
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
)
const muSig2CleanupTimeout = 5 * time.Second
// InstantOut holds the necessary information to execute an instant out swap.
type InstantOut struct {
// SwapHash is the hash of the swap.
@ -112,7 +115,10 @@ func (i *InstantOut) createMusig2Session(ctx context.Context,
for idx, reservation := range i.Reservations {
session, err := reservation.Musig2CreateSession(ctx, signer)
if err != nil {
return nil, nil, err
cleanupErr := cleanupMuSig2Sessions(
ctx, signer, musig2Sessions[:idx],
)
return nil, nil, errors.Join(err, cleanupErr)
}
musig2Sessions[idx] = session
@ -122,6 +128,32 @@ func (i *InstantOut) createMusig2Session(ctx context.Context,
return musig2Sessions, clientNonces, nil
}
// cleanupMuSig2Sessions removes completed or abandoned MuSig2 sessions from
// lnd. Cleanup uses a bounded context that survives cancellation of the swap
// action that created the sessions.
func cleanupMuSig2Sessions(ctx context.Context, signer lndclient.SignerClient,
sessions []*input.MuSig2SessionInfo) error {
cleanupCtx, cancel := context.WithTimeout(
context.WithoutCancel(ctx), muSig2CleanupTimeout,
)
defer cancel()
var cleanupErr error
for _, session := range sessions {
if session == nil {
continue
}
err := signer.MuSig2Cleanup(cleanupCtx, session.SessionID)
if err != nil {
cleanupErr = errors.Join(cleanupErr, err)
}
}
return cleanupErr
}
// getInputReservations returns the input reservations for the instant out.
func (i *InstantOut) getInputReservations() (InputReservations, error) {
if len(i.Reservations) == 0 {
@ -384,6 +416,11 @@ func (i *InstantOut) finalizeMusig2Transaction(ctx context.Context,
return nil, fmt.Errorf("missing sigs")
}
// lnd removes a MuSig2 session automatically once all signatures
// have been combined. Clear the local entry so the caller's deferred
// cleanup only targets sessions abandoned on an error path.
musig2Sessions[idx] = nil
tx.TxIn[idx].Witness = wire.TxWitness{finalSig}
vm, err := txscript.NewEngine(

View file

@ -23,6 +23,19 @@ func (s *invalidFinalSigSigner) MuSig2CombineSig(context.Context, [32]byte,
return true, make([]byte, 64), nil
}
type cleanupTrackingSigner struct {
lndclient.SignerClient
cleaned [][32]byte
}
func (s *cleanupTrackingSigner) MuSig2Cleanup(_ context.Context,
sessionID [32]byte) error {
s.cleaned = append(s.cleaned, sessionID)
return nil
}
// TestMuSig2VectorLengthValidation verifies that malformed server-controlled
// vectors are rejected before they can be indexed.
func TestMuSig2VectorLengthValidation(t *testing.T) {
@ -76,9 +89,27 @@ func TestFinalizeMuSig2TransactionVerifiesSignature(t *testing.T) {
tx.AddTxIn(&wire.TxIn{PreviousOutPoint: *res.Outpoint})
tx.AddTxOut(&wire.TxOut{Value: 90_000})
sessions := []*input.MuSig2SessionInfo{{}}
_, err := instantOut.finalizeMusig2Transaction(
context.Background(), &invalidFinalSigSigner{},
[]*input.MuSig2SessionInfo{{}}, tx, [][]byte{{1}},
sessions, tx, [][]byte{{1}},
)
require.ErrorContains(t, err, "invalid final MuSig2 signature")
require.Nil(t, sessions[0])
}
// TestCleanupMuSig2Sessions verifies that all allocated sessions are released
// while nil entries from partial session creation are skipped.
func TestCleanupMuSig2Sessions(t *testing.T) {
firstID := [32]byte{1}
secondID := [32]byte{2}
signer := &cleanupTrackingSigner{}
err := cleanupMuSig2Sessions(
t.Context(), signer, []*input.MuSig2SessionInfo{
{SessionID: firstID}, nil, {SessionID: secondID},
},
)
require.NoError(t, err)
require.Equal(t, [][32]byte{firstID, secondID}, signer.cleaned)
}