From 33917e1dbd9f0489bd153214f42c23ca63271b27 Mon Sep 17 00:00:00 2001 From: Boris Nagaev Date: Tue, 12 May 2026 02:09:56 -0500 Subject: [PATCH] loopd: prefer stored destination in sweephtlc --- loopd/sweep_htlc.go | 54 ++++++++++++++++++-------------- loopd/sweep_htlc_test.go | 66 +++++++++++++++++++++++++++++++--------- 2 files changed, 83 insertions(+), 37 deletions(-) diff --git a/loopd/sweep_htlc.go b/loopd/sweep_htlc.go index c613b5d7..a95727f3 100644 --- a/loopd/sweep_htlc.go +++ b/loopd/sweep_htlc.go @@ -106,8 +106,8 @@ func sweepHtlc(ctx context.Context, req *looprpc.SweepHtlcRequest, return nil, status.Error(codes.InvalidArgument, err.Error()) } - // Destination address: honor a provided override or derive a fresh - // wallet address from the default account. + // Destination address: honor a provided override immediately so request + // validation stays independent from swap lookup. var sweepAddr btcutil.Address if req.DestAddress != "" { sweepAddr, err = btcutil.DecodeAddress( @@ -117,30 +117,10 @@ func sweepHtlc(ctx context.Context, req *looprpc.SweepHtlcRequest, return nil, status.Errorf(codes.InvalidArgument, "invalid dest_address: %v", err) } - } else { - sweepAddr, err = wallet.NextAddr( - ctx, lnwallet.DefaultAccountName, - walletrpc.AddressType_TAPROOT_PUBKEY, - false, - ) - if err != nil { - return nil, status.Errorf(codes.Internal, - "derive sweep address: %v", err) - } - infof("sweephtlc: generated new destination address: %v", - sweepAddr.EncodeAddress()) } - sweepPkScript, err := txscript.PayToAddrScript(sweepAddr) - if err != nil { - return nil, err - } - - infof("sweephtlc: start sweep for %v -> %v", req.Outpoint, - sweepAddr.EncodeAddress()) - // Locate the loop-out swap whose HTLC script matches the outpoint so - // we can obtain keys and the stored preimage. + // we can obtain keys, the stored preimage, and the default destination. swaps, err := store.FetchLoopOutSwaps(ctx) if err != nil { return nil, err @@ -172,6 +152,34 @@ func sweepHtlc(ctx context.Context, req *looprpc.SweepHtlcRequest, "no matching swap HTLC found") } + // Prefer the stored swap destination for recovery sweeps and only + // derive a fresh wallet address when neither the request nor DB + // specifies one. + if sweepAddr == nil { + sweepAddr = targetSwap.Contract.DestAddr + } + if sweepAddr == nil { + sweepAddr, err = wallet.NextAddr( + ctx, lnwallet.DefaultAccountName, + walletrpc.AddressType_TAPROOT_PUBKEY, + false, + ) + if err != nil { + return nil, status.Errorf(codes.Internal, + "derive sweep address: %v", err) + } + infof("sweephtlc: generated new destination address: %v", + sweepAddr.EncodeAddress()) + } + + sweepPkScript, err := txscript.PayToAddrScript(sweepAddr) + if err != nil { + return nil, err + } + + infof("sweephtlc: start sweep for %v -> %v", req.Outpoint, + sweepAddr.EncodeAddress()) + infof("sweephtlc: matched swap %v at height hint %v", targetSwap.Hash, targetSwap.Contract.InitiationHeight) diff --git a/loopd/sweep_htlc_test.go b/loopd/sweep_htlc_test.go index 139eca82..7be675cc 100644 --- a/loopd/sweep_htlc_test.go +++ b/loopd/sweep_htlc_test.go @@ -8,6 +8,7 @@ import ( "time" "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btclog/v2" "github.com/lightninglabs/loop/loopdb" @@ -17,7 +18,9 @@ import ( "github.com/lightninglabs/loop/utils" "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/lnrpc/walletrpc" "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/stretchr/testify/require" ) @@ -45,7 +48,6 @@ var sweepHtlcTests = []struct { satPerVByte: 10, expectRegister: true, expectLogs: []string{ - "sweephtlc: generated new destination address: %v", "sweephtlc: start sweep for %v -> %v", "sweephtlc: matched swap %v at height hint %v", "sweephtlc: registering conf ntfn for %v hint=%v", @@ -63,7 +65,6 @@ var sweepHtlcTests = []struct { satPerVByte: 10, expectRegister: true, expectLogs: []string{ - "sweephtlc: generated new destination address: %v", "sweephtlc: start sweep for %v -> %v", "sweephtlc: matched swap %v at height hint %v", "sweephtlc: registering conf ntfn for %v hint=%v", @@ -83,7 +84,6 @@ var sweepHtlcTests = []struct { satPerVByte: 10, expectRegister: true, expectLogs: []string{ - "sweephtlc: generated new destination address: %v", "sweephtlc: start sweep for %v -> %v", "sweephtlc: matched swap %v at height hint %v", "sweephtlc: registering conf ntfn for %v hint=%v", @@ -105,7 +105,6 @@ var sweepHtlcTests = []struct { expectErrMsg: "fee exceeds", expectRegister: true, expectLogs: []string{ - "sweephtlc: generated new destination address: %v", "sweephtlc: start sweep for %v -> %v", "sweephtlc: matched swap %v at height hint %v", "sweephtlc: registering conf ntfn for %v hint=%v", @@ -124,7 +123,6 @@ var sweepHtlcTests = []struct { expectErrMsg: "fee too low for relay after clamp", expectRegister: true, expectLogs: []string{ - "sweephtlc: generated new destination address: %v", "sweephtlc: start sweep for %v -> %v", "sweephtlc: matched swap %v at height hint %v", "sweephtlc: registering conf ntfn for %v hint=%v", @@ -182,10 +180,7 @@ var sweepHtlcTests = []struct { expectErrMsg: "no matching swap", expectRegister: false, noSwap: true, - expectLogs: []string{ - "sweephtlc: generated new destination address: %v", - "sweephtlc: start sweep for %v -> %v", - }, + expectLogs: []string{}, }, { name: "invalid initiation height", @@ -197,7 +192,6 @@ var sweepHtlcTests = []struct { contract.InitiationHeight = 0 }, expectLogs: []string{ - "sweephtlc: generated new destination address: %v", "sweephtlc: start sweep for %v -> %v", "sweephtlc: matched swap %v at height hint %v", }, @@ -212,7 +206,6 @@ var sweepHtlcTests = []struct { reg.ErrChan <- errors.New("boom") }, expectLogs: []string{ - "sweephtlc: generated new destination address: %v", "sweephtlc: start sweep for %v -> %v", "sweephtlc: matched swap %v at height hint %v", "sweephtlc: registering conf ntfn for %v hint=%v", @@ -230,7 +223,6 @@ var sweepHtlcTests = []struct { txOut.PkScript = []byte{0x6a} }, expectLogs: []string{ - "sweephtlc: generated new destination address: %v", "sweephtlc: start sweep for %v -> %v", "sweephtlc: matched swap %v at height hint %v", "sweephtlc: registering conf ntfn for %v hint=%v", @@ -245,7 +237,6 @@ var sweepHtlcTests = []struct { expectErrMsg: "fee exceeds HTLC value", expectRegister: true, expectLogs: []string{ - "sweephtlc: generated new destination address: %v", "sweephtlc: start sweep for %v -> %v", "sweephtlc: matched swap %v at height hint %v", "sweephtlc: registering conf ntfn for %v hint=%v", @@ -264,6 +255,23 @@ var sweepHtlcTests = []struct { modifyReq: func(req *looprpc.SweepHtlcRequest) { req.Preimage = bytes.Repeat([]byte{9}, 32) }, + expectLogs: []string{ + "sweephtlc: start sweep for %v -> %v", + "sweephtlc: matched swap %v at height hint %v", + "sweephtlc: registering conf ntfn for %v hint=%v", + "sweephtlc: waiting for confirmation of %v", + "sweephtlc: funding confirmed at height %v", + "sweephtlc: swap hash validated for %v", + }, + }, + { + name: "fallback to generated destination", + amount: 100_000, + satPerVByte: 10, + expectRegister: true, + mutateSwap: func(contract *loopdb.LoopOutContract) { + contract.DestAddr = nil + }, expectLogs: []string{ "sweephtlc: generated new destination address: %v", "sweephtlc: start sweep for %v -> %v", @@ -272,6 +280,9 @@ var sweepHtlcTests = []struct { "sweephtlc: waiting for confirmation of %v", "sweephtlc: funding confirmed at height %v", "sweephtlc: swap hash validated for %v", + "sweephtlc: sweeping to %v with feerate %v sat/vbyte", + "sweephtlc: signing sweep spending %v", + "sweephtlc: witness assembled, tx size=%d vbytes", }, }, } @@ -336,7 +347,7 @@ func TestSweepHtlc(t *testing.T) { } destAddr, err := btcutil.NewAddressWitnessPubKeyHash( - make([]byte, 20), lnd.ChainParams, + bytes.Repeat([]byte{1}, 20), lnd.ChainParams, ) require.NoError(t, err) @@ -498,6 +509,33 @@ func TestSweepHtlc(t *testing.T) { ) require.NotEmpty(t, sweepTx.TxIn[0].Witness) + // Verify that the sweep uses the stored destination address and + // only falls back to wallet address generation when the swap + // record does not define one. + expectedAddr := loopOut.Contract.DestAddr + if req.DestAddress != "" { + expectedAddr, err = btcutil.DecodeAddress( + req.DestAddress, lnd.ChainParams, + ) + require.NoError(t, err) + } + if expectedAddr == nil { + expectedAddr, err = lnd.WalletKit.NextAddr( + ctx, lnwallet.DefaultAccountName, + walletrpc.AddressType_TAPROOT_PUBKEY, false, + ) + require.NoError(t, err) + } + + expectedPkScript, err := txscript.PayToAddrScript( + expectedAddr, + ) + require.NoError(t, err) + require.Len(t, sweepTx.TxOut, 1) + require.Equal( + t, expectedPkScript, sweepTx.TxOut[0].PkScript, + ) + if tc.publish { // For publish=true we should see a // publish (or a publish failure