loopd: prefer stored destination in sweephtlc

This commit is contained in:
Boris Nagaev 2026-05-12 02:09:56 -05:00
parent 6c57f05d8a
commit 33917e1dbd
No known key found for this signature in database
2 changed files with 83 additions and 37 deletions

View file

@ -106,8 +106,8 @@ func sweepHtlc(ctx context.Context, req *looprpc.SweepHtlcRequest,
return nil, status.Error(codes.InvalidArgument, err.Error()) return nil, status.Error(codes.InvalidArgument, err.Error())
} }
// Destination address: honor a provided override or derive a fresh // Destination address: honor a provided override immediately so request
// wallet address from the default account. // validation stays independent from swap lookup.
var sweepAddr btcutil.Address var sweepAddr btcutil.Address
if req.DestAddress != "" { if req.DestAddress != "" {
sweepAddr, err = btcutil.DecodeAddress( sweepAddr, err = btcutil.DecodeAddress(
@ -117,30 +117,10 @@ func sweepHtlc(ctx context.Context, req *looprpc.SweepHtlcRequest,
return nil, status.Errorf(codes.InvalidArgument, return nil, status.Errorf(codes.InvalidArgument,
"invalid dest_address: %v", err) "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 // 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) swaps, err := store.FetchLoopOutSwaps(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
@ -172,6 +152,34 @@ func sweepHtlc(ctx context.Context, req *looprpc.SweepHtlcRequest,
"no matching swap HTLC found") "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", infof("sweephtlc: matched swap %v at height hint %v",
targetSwap.Hash, targetSwap.Contract.InitiationHeight) targetSwap.Hash, targetSwap.Contract.InitiationHeight)

View file

@ -8,6 +8,7 @@ import (
"time" "time"
"github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btclog/v2" "github.com/btcsuite/btclog/v2"
"github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/loopdb"
@ -17,7 +18,9 @@ import (
"github.com/lightninglabs/loop/utils" "github.com/lightninglabs/loop/utils"
"github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
"github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -45,7 +48,6 @@ var sweepHtlcTests = []struct {
satPerVByte: 10, satPerVByte: 10,
expectRegister: true, expectRegister: true,
expectLogs: []string{ expectLogs: []string{
"sweephtlc: generated new destination address: %v",
"sweephtlc: start sweep for %v -> %v", "sweephtlc: start sweep for %v -> %v",
"sweephtlc: matched swap %v at height hint %v", "sweephtlc: matched swap %v at height hint %v",
"sweephtlc: registering conf ntfn for %v hint=%v", "sweephtlc: registering conf ntfn for %v hint=%v",
@ -63,7 +65,6 @@ var sweepHtlcTests = []struct {
satPerVByte: 10, satPerVByte: 10,
expectRegister: true, expectRegister: true,
expectLogs: []string{ expectLogs: []string{
"sweephtlc: generated new destination address: %v",
"sweephtlc: start sweep for %v -> %v", "sweephtlc: start sweep for %v -> %v",
"sweephtlc: matched swap %v at height hint %v", "sweephtlc: matched swap %v at height hint %v",
"sweephtlc: registering conf ntfn for %v hint=%v", "sweephtlc: registering conf ntfn for %v hint=%v",
@ -83,7 +84,6 @@ var sweepHtlcTests = []struct {
satPerVByte: 10, satPerVByte: 10,
expectRegister: true, expectRegister: true,
expectLogs: []string{ expectLogs: []string{
"sweephtlc: generated new destination address: %v",
"sweephtlc: start sweep for %v -> %v", "sweephtlc: start sweep for %v -> %v",
"sweephtlc: matched swap %v at height hint %v", "sweephtlc: matched swap %v at height hint %v",
"sweephtlc: registering conf ntfn for %v hint=%v", "sweephtlc: registering conf ntfn for %v hint=%v",
@ -105,7 +105,6 @@ var sweepHtlcTests = []struct {
expectErrMsg: "fee exceeds", expectErrMsg: "fee exceeds",
expectRegister: true, expectRegister: true,
expectLogs: []string{ expectLogs: []string{
"sweephtlc: generated new destination address: %v",
"sweephtlc: start sweep for %v -> %v", "sweephtlc: start sweep for %v -> %v",
"sweephtlc: matched swap %v at height hint %v", "sweephtlc: matched swap %v at height hint %v",
"sweephtlc: registering conf ntfn for %v 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", expectErrMsg: "fee too low for relay after clamp",
expectRegister: true, expectRegister: true,
expectLogs: []string{ expectLogs: []string{
"sweephtlc: generated new destination address: %v",
"sweephtlc: start sweep for %v -> %v", "sweephtlc: start sweep for %v -> %v",
"sweephtlc: matched swap %v at height hint %v", "sweephtlc: matched swap %v at height hint %v",
"sweephtlc: registering conf ntfn for %v hint=%v", "sweephtlc: registering conf ntfn for %v hint=%v",
@ -182,10 +180,7 @@ var sweepHtlcTests = []struct {
expectErrMsg: "no matching swap", expectErrMsg: "no matching swap",
expectRegister: false, expectRegister: false,
noSwap: true, noSwap: true,
expectLogs: []string{ expectLogs: []string{},
"sweephtlc: generated new destination address: %v",
"sweephtlc: start sweep for %v -> %v",
},
}, },
{ {
name: "invalid initiation height", name: "invalid initiation height",
@ -197,7 +192,6 @@ var sweepHtlcTests = []struct {
contract.InitiationHeight = 0 contract.InitiationHeight = 0
}, },
expectLogs: []string{ expectLogs: []string{
"sweephtlc: generated new destination address: %v",
"sweephtlc: start sweep for %v -> %v", "sweephtlc: start sweep for %v -> %v",
"sweephtlc: matched swap %v at height hint %v", "sweephtlc: matched swap %v at height hint %v",
}, },
@ -212,7 +206,6 @@ var sweepHtlcTests = []struct {
reg.ErrChan <- errors.New("boom") reg.ErrChan <- errors.New("boom")
}, },
expectLogs: []string{ expectLogs: []string{
"sweephtlc: generated new destination address: %v",
"sweephtlc: start sweep for %v -> %v", "sweephtlc: start sweep for %v -> %v",
"sweephtlc: matched swap %v at height hint %v", "sweephtlc: matched swap %v at height hint %v",
"sweephtlc: registering conf ntfn for %v hint=%v", "sweephtlc: registering conf ntfn for %v hint=%v",
@ -230,7 +223,6 @@ var sweepHtlcTests = []struct {
txOut.PkScript = []byte{0x6a} txOut.PkScript = []byte{0x6a}
}, },
expectLogs: []string{ expectLogs: []string{
"sweephtlc: generated new destination address: %v",
"sweephtlc: start sweep for %v -> %v", "sweephtlc: start sweep for %v -> %v",
"sweephtlc: matched swap %v at height hint %v", "sweephtlc: matched swap %v at height hint %v",
"sweephtlc: registering conf ntfn for %v hint=%v", "sweephtlc: registering conf ntfn for %v hint=%v",
@ -245,7 +237,6 @@ var sweepHtlcTests = []struct {
expectErrMsg: "fee exceeds HTLC value", expectErrMsg: "fee exceeds HTLC value",
expectRegister: true, expectRegister: true,
expectLogs: []string{ expectLogs: []string{
"sweephtlc: generated new destination address: %v",
"sweephtlc: start sweep for %v -> %v", "sweephtlc: start sweep for %v -> %v",
"sweephtlc: matched swap %v at height hint %v", "sweephtlc: matched swap %v at height hint %v",
"sweephtlc: registering conf ntfn for %v hint=%v", "sweephtlc: registering conf ntfn for %v hint=%v",
@ -264,6 +255,23 @@ var sweepHtlcTests = []struct {
modifyReq: func(req *looprpc.SweepHtlcRequest) { modifyReq: func(req *looprpc.SweepHtlcRequest) {
req.Preimage = bytes.Repeat([]byte{9}, 32) 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{ expectLogs: []string{
"sweephtlc: generated new destination address: %v", "sweephtlc: generated new destination address: %v",
"sweephtlc: start sweep for %v -> %v", "sweephtlc: start sweep for %v -> %v",
@ -272,6 +280,9 @@ var sweepHtlcTests = []struct {
"sweephtlc: waiting for confirmation of %v", "sweephtlc: waiting for confirmation of %v",
"sweephtlc: funding confirmed at height %v", "sweephtlc: funding confirmed at height %v",
"sweephtlc: swap hash validated for %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( destAddr, err := btcutil.NewAddressWitnessPubKeyHash(
make([]byte, 20), lnd.ChainParams, bytes.Repeat([]byte{1}, 20), lnd.ChainParams,
) )
require.NoError(t, err) require.NoError(t, err)
@ -498,6 +509,33 @@ func TestSweepHtlc(t *testing.T) {
) )
require.NotEmpty(t, sweepTx.TxIn[0].Witness) 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 { if tc.publish {
// For publish=true we should see a // For publish=true we should see a
// publish (or a publish failure // publish (or a publish failure