2026-01-14 18:30:38 -05:00
|
|
|
package loopd
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"context"
|
|
|
|
|
"errors"
|
|
|
|
|
"testing"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/btcsuite/btcd/btcutil"
|
2026-05-12 02:09:56 -05:00
|
|
|
"github.com/btcsuite/btcd/txscript"
|
2026-01-14 18:30:38 -05:00
|
|
|
"github.com/btcsuite/btcd/wire"
|
|
|
|
|
"github.com/btcsuite/btclog/v2"
|
|
|
|
|
"github.com/lightninglabs/loop/loopdb"
|
|
|
|
|
"github.com/lightninglabs/loop/looprpc"
|
|
|
|
|
"github.com/lightninglabs/loop/swap"
|
|
|
|
|
"github.com/lightninglabs/loop/test"
|
|
|
|
|
"github.com/lightninglabs/loop/utils"
|
|
|
|
|
"github.com/lightningnetwork/lnd/chainntnfs"
|
|
|
|
|
"github.com/lightningnetwork/lnd/keychain"
|
2026-05-12 02:09:56 -05:00
|
|
|
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
|
2026-01-14 18:30:38 -05:00
|
|
|
"github.com/lightningnetwork/lnd/lntypes"
|
2026-05-12 02:09:56 -05:00
|
|
|
"github.com/lightningnetwork/lnd/lnwallet"
|
2026-01-14 18:30:38 -05:00
|
|
|
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// sweepHtlcTests is a collection of table tests for TestSweepHtlc.
|
|
|
|
|
var sweepHtlcTests = []struct {
|
|
|
|
|
name string
|
|
|
|
|
amount btcutil.Amount
|
|
|
|
|
satPerVByte uint32
|
|
|
|
|
minRelayFee chainfee.SatPerKWeight
|
|
|
|
|
expectErrMsg string
|
|
|
|
|
expectLogs []string
|
|
|
|
|
expectRegister bool
|
|
|
|
|
noSwap bool
|
|
|
|
|
publish bool
|
|
|
|
|
publishErr bool
|
|
|
|
|
modifyReq func(*looprpc.SweepHtlcRequest)
|
|
|
|
|
mutateSwap func(*loopdb.LoopOutContract)
|
|
|
|
|
mutateTxOut func(*wire.TxOut)
|
|
|
|
|
sendConf func(*test.ConfRegistration)
|
|
|
|
|
}{
|
|
|
|
|
{
|
|
|
|
|
name: "success low fee",
|
|
|
|
|
amount: 100_000,
|
|
|
|
|
satPerVByte: 10,
|
|
|
|
|
expectRegister: true,
|
|
|
|
|
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",
|
|
|
|
|
"sweephtlc: sweeping to %v with feerate %v sat/vbyte",
|
|
|
|
|
"sweephtlc: signing sweep spending %v",
|
|
|
|
|
"sweephtlc: witness assembled, tx size=%d vbytes",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "success low fee, publish",
|
|
|
|
|
amount: 100_000,
|
|
|
|
|
satPerVByte: 10,
|
|
|
|
|
expectRegister: true,
|
|
|
|
|
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",
|
|
|
|
|
"sweephtlc: sweeping to %v with feerate %v sat/vbyte",
|
|
|
|
|
"sweephtlc: signing sweep spending %v",
|
|
|
|
|
"sweephtlc: witness assembled, tx size=%d vbytes",
|
|
|
|
|
"sweephtlc: published sweep %v",
|
|
|
|
|
},
|
|
|
|
|
publish: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "publish failure reported",
|
|
|
|
|
amount: 100_000,
|
|
|
|
|
satPerVByte: 10,
|
|
|
|
|
expectRegister: true,
|
|
|
|
|
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",
|
|
|
|
|
"sweephtlc: sweeping to %v with feerate %v sat/vbyte",
|
|
|
|
|
"sweephtlc: signing sweep spending %v",
|
|
|
|
|
"sweephtlc: witness assembled, tx size=%d vbytes",
|
|
|
|
|
"sweephtlc: publish failed for %v: %v",
|
|
|
|
|
},
|
|
|
|
|
publish: true,
|
|
|
|
|
publishErr: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "fee clamped over ratio",
|
|
|
|
|
amount: 100_000,
|
|
|
|
|
satPerVByte: 200,
|
|
|
|
|
expectErrMsg: "fee exceeds",
|
|
|
|
|
expectRegister: true,
|
|
|
|
|
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",
|
|
|
|
|
"sweephtlc: sweeping to %v with feerate %v sat/vbyte",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "clamped below min relay",
|
|
|
|
|
amount: 10_000,
|
|
|
|
|
// Will clamp further.
|
|
|
|
|
satPerVByte: 5,
|
|
|
|
|
minRelayFee: chainfee.SatPerKWeight(1_000_000),
|
|
|
|
|
expectErrMsg: "fee too low for relay after clamp",
|
|
|
|
|
expectRegister: true,
|
|
|
|
|
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",
|
|
|
|
|
"sweephtlc: sweeping to %v with feerate %v sat/vbyte",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "missing outpoint",
|
|
|
|
|
amount: 100_000,
|
|
|
|
|
satPerVByte: 10,
|
|
|
|
|
expectErrMsg: "outpoint required",
|
|
|
|
|
expectLogs: []string{},
|
|
|
|
|
expectRegister: false,
|
|
|
|
|
modifyReq: func(req *looprpc.SweepHtlcRequest) {
|
|
|
|
|
req.Outpoint = ""
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "missing htlc address",
|
|
|
|
|
amount: 100_000,
|
|
|
|
|
satPerVByte: 10,
|
|
|
|
|
expectErrMsg: "htlc_address required",
|
|
|
|
|
expectLogs: []string{},
|
|
|
|
|
expectRegister: false,
|
|
|
|
|
modifyReq: func(req *looprpc.SweepHtlcRequest) {
|
|
|
|
|
req.HtlcAddress = ""
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "missing feerate",
|
|
|
|
|
amount: 100_000,
|
|
|
|
|
satPerVByte: 0,
|
|
|
|
|
expectErrMsg: "sat_per_vbyte required",
|
|
|
|
|
expectLogs: []string{},
|
|
|
|
|
expectRegister: false,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "invalid htlc address",
|
|
|
|
|
amount: 100_000,
|
|
|
|
|
satPerVByte: 10,
|
|
|
|
|
expectErrMsg: "invalid htlc_address",
|
|
|
|
|
expectLogs: []string{},
|
|
|
|
|
expectRegister: false,
|
|
|
|
|
modifyReq: func(req *looprpc.SweepHtlcRequest) {
|
|
|
|
|
req.HtlcAddress = "notanaddress"
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "no matching swap",
|
|
|
|
|
amount: 100_000,
|
|
|
|
|
satPerVByte: 10,
|
|
|
|
|
expectErrMsg: "no matching swap",
|
|
|
|
|
expectRegister: false,
|
|
|
|
|
noSwap: true,
|
2026-05-12 02:09:56 -05:00
|
|
|
expectLogs: []string{},
|
2026-01-14 18:30:38 -05:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "invalid initiation height",
|
|
|
|
|
amount: 100_000,
|
|
|
|
|
satPerVByte: 10,
|
|
|
|
|
expectErrMsg: "invalid initiation height",
|
|
|
|
|
expectRegister: false,
|
|
|
|
|
mutateSwap: func(contract *loopdb.LoopOutContract) {
|
|
|
|
|
contract.InitiationHeight = 0
|
|
|
|
|
},
|
|
|
|
|
expectLogs: []string{
|
|
|
|
|
"sweephtlc: start sweep for %v -> %v",
|
|
|
|
|
"sweephtlc: matched swap %v at height hint %v",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "conf ntfn error",
|
|
|
|
|
amount: 100_000,
|
|
|
|
|
satPerVByte: 10,
|
|
|
|
|
expectErrMsg: "conf ntfn",
|
|
|
|
|
expectRegister: true,
|
|
|
|
|
sendConf: func(reg *test.ConfRegistration) {
|
|
|
|
|
reg.ErrChan <- errors.New("boom")
|
|
|
|
|
},
|
|
|
|
|
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: conf ntfn error for %v: %v",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "outpoint script mismatch",
|
|
|
|
|
amount: 100_000,
|
|
|
|
|
satPerVByte: 10,
|
|
|
|
|
expectErrMsg: "outpoint script does not match HTLC address",
|
|
|
|
|
expectRegister: true,
|
|
|
|
|
mutateTxOut: func(txOut *wire.TxOut) {
|
|
|
|
|
txOut.PkScript = []byte{0x6a}
|
|
|
|
|
},
|
|
|
|
|
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",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "fee exceeds htlc value",
|
|
|
|
|
amount: 100_000,
|
|
|
|
|
satPerVByte: 2_000_000,
|
|
|
|
|
expectErrMsg: "fee exceeds HTLC value",
|
|
|
|
|
expectRegister: true,
|
|
|
|
|
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",
|
|
|
|
|
"sweephtlc: sweeping to %v with feerate %v sat/vbyte",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "preimage mismatch",
|
|
|
|
|
amount: 100_000,
|
|
|
|
|
satPerVByte: 10,
|
|
|
|
|
expectErrMsg: "preimage does not match HTLC hash",
|
|
|
|
|
expectRegister: true,
|
|
|
|
|
modifyReq: func(req *looprpc.SweepHtlcRequest) {
|
|
|
|
|
req.Preimage = bytes.Repeat([]byte{9}, 32)
|
|
|
|
|
},
|
2026-05-12 02:09:56 -05:00
|
|
|
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
|
|
|
|
|
},
|
2026-01-14 18:30:38 -05:00
|
|
|
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",
|
|
|
|
|
"sweephtlc: waiting for confirmation of %v",
|
|
|
|
|
"sweephtlc: funding confirmed at height %v",
|
|
|
|
|
"sweephtlc: swap hash validated for %v",
|
2026-05-12 02:09:56 -05:00
|
|
|
"sweephtlc: sweeping to %v with feerate %v sat/vbyte",
|
|
|
|
|
"sweephtlc: signing sweep spending %v",
|
|
|
|
|
"sweephtlc: witness assembled, tx size=%d vbytes",
|
2026-01-14 18:30:38 -05:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestSweepHtlc runs a table of happy-path and fee-related rejection cases for
|
|
|
|
|
// the sweep helper.
|
|
|
|
|
func TestSweepHtlc(t *testing.T) {
|
|
|
|
|
// shortDelay is used to check that nothing is produced from a channel.
|
|
|
|
|
const shortDelay = 100 * time.Millisecond
|
|
|
|
|
|
|
|
|
|
for _, tc := range sweepHtlcTests {
|
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
|
|
|
// Catch leaked goroutines and constrain test time.
|
|
|
|
|
defer test.Guard(t)()
|
|
|
|
|
|
|
|
|
|
// Fresh logger per test to capture emitted formats.
|
|
|
|
|
logger := newFormatLogger()
|
|
|
|
|
setLogger(logger)
|
|
|
|
|
|
|
|
|
|
// Base mocks for wallet/notifier/signer.
|
|
|
|
|
lnd := test.NewMockLnd()
|
|
|
|
|
if tc.publishErr {
|
|
|
|
|
lnd.PublishHandler = func(ctx context.Context,
|
|
|
|
|
_ *wire.MsgTx, _ string) error {
|
|
|
|
|
|
|
|
|
|
return errors.New("publish-fail")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if tc.minRelayFee != 0 {
|
|
|
|
|
lnd.SetMinRelayFee(tc.minRelayFee)
|
|
|
|
|
}
|
|
|
|
|
store := loopdb.NewStoreMock(t)
|
|
|
|
|
|
|
|
|
|
preimage := lntypes.Preimage{1, 2, 3, 4}
|
|
|
|
|
swapHash := preimage.Hash()
|
|
|
|
|
|
|
|
|
|
_, senderPub := test.CreateKey(0)
|
|
|
|
|
_, receiverPub := test.CreateKey(1)
|
|
|
|
|
|
|
|
|
|
var senderKey, receiverKey [33]byte
|
|
|
|
|
copy(senderKey[:], senderPub.SerializeCompressed())
|
|
|
|
|
copy(receiverKey[:], receiverPub.SerializeCompressed())
|
|
|
|
|
|
|
|
|
|
htlcKeys := loopdb.HtlcKeys{
|
|
|
|
|
SenderScriptKey: senderKey,
|
|
|
|
|
ReceiverScriptKey: receiverKey,
|
|
|
|
|
ClientScriptKeyLocator: keychain.KeyLocator{
|
|
|
|
|
Family: keychain.KeyFamily(
|
|
|
|
|
swap.KeyFamily,
|
|
|
|
|
),
|
|
|
|
|
Index: 0,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
swapContract := loopdb.SwapContract{
|
|
|
|
|
Preimage: preimage,
|
|
|
|
|
AmountRequested: tc.amount,
|
|
|
|
|
HtlcKeys: htlcKeys,
|
|
|
|
|
CltvExpiry: 500,
|
|
|
|
|
InitiationHeight: 123,
|
|
|
|
|
ProtocolVersion: loopdb.ProtocolVersionHtlcV2,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
destAddr, err := btcutil.NewAddressWitnessPubKeyHash(
|
2026-05-12 02:09:56 -05:00
|
|
|
bytes.Repeat([]byte{1}, 20), lnd.ChainParams,
|
2026-01-14 18:30:38 -05:00
|
|
|
)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
loopOut := &loopdb.LoopOut{
|
|
|
|
|
Loop: loopdb.Loop{
|
|
|
|
|
Hash: swapHash,
|
|
|
|
|
},
|
|
|
|
|
Contract: &loopdb.LoopOutContract{
|
|
|
|
|
SwapContract: swapContract,
|
|
|
|
|
DestAddr: destAddr,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Store the swap unless this case disables it.
|
|
|
|
|
if tc.mutateSwap != nil {
|
|
|
|
|
tc.mutateSwap(loopOut.Contract)
|
|
|
|
|
}
|
|
|
|
|
if !tc.noSwap {
|
|
|
|
|
store.LoopOutSwaps[swapHash] = loopOut.Contract
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Build HTLC details and funding tx.
|
|
|
|
|
htlc, err := utils.GetHtlc(
|
|
|
|
|
swapHash, &loopOut.Contract.SwapContract,
|
|
|
|
|
lnd.ChainParams,
|
|
|
|
|
)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
fundingTx := wire.NewMsgTx(2)
|
|
|
|
|
txOut := &wire.TxOut{
|
|
|
|
|
Value: int64(
|
|
|
|
|
loopOut.Contract.AmountRequested,
|
|
|
|
|
),
|
|
|
|
|
PkScript: htlc.PkScript,
|
|
|
|
|
}
|
|
|
|
|
if tc.mutateTxOut != nil {
|
|
|
|
|
tc.mutateTxOut(txOut)
|
|
|
|
|
}
|
|
|
|
|
fundingTx.AddTxOut(txOut)
|
|
|
|
|
fundingHash := fundingTx.TxHash()
|
|
|
|
|
outpoint := wire.OutPoint{Hash: fundingHash, Index: 0}
|
|
|
|
|
|
|
|
|
|
ctx, cancel := context.WithTimeout(
|
|
|
|
|
t.Context(), 5*time.Second,
|
|
|
|
|
)
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
// Drain signer requests to avoid blocking.
|
|
|
|
|
go func() {
|
|
|
|
|
select {
|
|
|
|
|
case <-lnd.SignOutputRawChannel:
|
|
|
|
|
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
pubChan := make(chan *wire.MsgTx, 1)
|
|
|
|
|
|
|
|
|
|
// If publish is requested, drain TxPublishChannel so
|
|
|
|
|
// the mock PublishTransaction does not block.
|
|
|
|
|
if tc.publish {
|
|
|
|
|
go func() {
|
|
|
|
|
select {
|
|
|
|
|
case tx := <-lnd.TxPublishChannel:
|
|
|
|
|
pubChan <- tx
|
|
|
|
|
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle confirmation registration caused by the call.
|
|
|
|
|
if tc.expectRegister {
|
|
|
|
|
// Consume notifier registration.
|
|
|
|
|
go func() {
|
|
|
|
|
var reg *test.ConfRegistration
|
|
|
|
|
select {
|
|
|
|
|
case reg = <-lnd.RegisterConfChannel:
|
|
|
|
|
// Got registration.
|
|
|
|
|
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Either send an error or a
|
|
|
|
|
// confirmation.
|
|
|
|
|
if tc.sendConf != nil {
|
|
|
|
|
tc.sendConf(reg)
|
|
|
|
|
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
conf := &chainntnfs.TxConfirmation{
|
|
|
|
|
Tx: fundingTx,
|
|
|
|
|
}
|
|
|
|
|
reg.ConfChan <- conf
|
|
|
|
|
}()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Build request with optional mutation.
|
|
|
|
|
req := &looprpc.SweepHtlcRequest{
|
|
|
|
|
Outpoint: outpoint.String(),
|
|
|
|
|
SatPerVbyte: tc.satPerVByte,
|
|
|
|
|
Publish: tc.publish,
|
|
|
|
|
HtlcAddress: htlc.Address.String(),
|
|
|
|
|
DestAddress: "",
|
|
|
|
|
Preimage: nil,
|
|
|
|
|
}
|
|
|
|
|
if tc.modifyReq != nil {
|
|
|
|
|
tc.modifyReq(req)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Invoke sweepHtlc and forward the result.
|
|
|
|
|
resp, err := sweepHtlc(
|
|
|
|
|
ctx, req, lnd.ChainParams, store,
|
|
|
|
|
lnd.ChainNotifier, lnd.WalletKit,
|
|
|
|
|
lnd.Signer,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Handle confirmation registration caused by the call
|
|
|
|
|
// when not expected.
|
|
|
|
|
if !tc.expectRegister {
|
|
|
|
|
select {
|
|
|
|
|
case reg := <-lnd.RegisterConfChannel:
|
|
|
|
|
t.Fatalf("unexpected registration: %+v",
|
|
|
|
|
reg)
|
|
|
|
|
|
|
|
|
|
case <-time.After(shortDelay):
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Make sure it produced the expected logs.
|
|
|
|
|
logs := logger.formats
|
|
|
|
|
if logs == nil {
|
|
|
|
|
logs = []string{}
|
|
|
|
|
}
|
|
|
|
|
require.Equal(t, tc.expectLogs, logs)
|
|
|
|
|
|
|
|
|
|
// Ensure all mock channels are drained.
|
|
|
|
|
defer require.NoError(t, lnd.IsDone())
|
|
|
|
|
|
|
|
|
|
// Error path.
|
|
|
|
|
if tc.expectErrMsg != "" {
|
|
|
|
|
require.ErrorContains(t, err, tc.expectErrMsg)
|
|
|
|
|
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Success path.
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
// Parse the produced signed transaction.
|
|
|
|
|
require.NotEmpty(t, resp.SweepTx)
|
|
|
|
|
var sweepTx wire.MsgTx
|
|
|
|
|
err = sweepTx.Deserialize(bytes.NewReader(resp.SweepTx))
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
require.Equal(
|
|
|
|
|
t, outpoint, sweepTx.TxIn[0].PreviousOutPoint,
|
|
|
|
|
)
|
|
|
|
|
require.NotEmpty(t, sweepTx.TxIn[0].Witness)
|
|
|
|
|
|
2026-05-12 02:09:56 -05:00
|
|
|
// 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,
|
|
|
|
|
)
|
|
|
|
|
|
2026-01-14 18:30:38 -05:00
|
|
|
if tc.publish {
|
|
|
|
|
// For publish=true we should see a
|
|
|
|
|
// publish (or a publish failure
|
|
|
|
|
// response which skips broadcast).
|
|
|
|
|
select {
|
|
|
|
|
case tx := <-pubChan:
|
|
|
|
|
require.NotNil(t, tx)
|
|
|
|
|
|
|
|
|
|
case <-time.After(shortDelay):
|
|
|
|
|
if !tc.publishErr {
|
|
|
|
|
t.Fatal("expected publish")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// For publish=false we should not
|
|
|
|
|
// publish.
|
|
|
|
|
select {
|
|
|
|
|
case <-lnd.TxPublishChannel:
|
|
|
|
|
t.Fatal("unexpected publish")
|
|
|
|
|
|
|
|
|
|
case <-time.After(shortDelay):
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// formatLogger captures format strings passed to the logger interface so we
|
|
|
|
|
// can assert on log invocations.
|
|
|
|
|
type formatLogger struct {
|
|
|
|
|
btclog.Logger
|
|
|
|
|
|
|
|
|
|
formats []string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// newFormatLogger builds a logger that records format strings while discarding
|
|
|
|
|
// actual log output.
|
|
|
|
|
func newFormatLogger() *formatLogger {
|
|
|
|
|
return &formatLogger{Logger: btclog.Disabled}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// record stores the raw format string.
|
|
|
|
|
func (f *formatLogger) record(format string) {
|
|
|
|
|
f.formats = append(f.formats, format)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Tracef logs a trace and records its format.
|
2026-03-05 12:18:05 +01:00
|
|
|
func (f *formatLogger) Tracef(format string, params ...any) {
|
2026-01-14 18:30:38 -05:00
|
|
|
f.record(format)
|
|
|
|
|
f.Logger.Tracef(format, params...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Debugf logs a debug message and records its format.
|
2026-03-05 12:18:05 +01:00
|
|
|
func (f *formatLogger) Debugf(format string, params ...any) {
|
2026-01-14 18:30:38 -05:00
|
|
|
f.record(format)
|
|
|
|
|
f.Logger.Debugf(format, params...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Infof logs an info message and records its format.
|
2026-03-05 12:18:05 +01:00
|
|
|
func (f *formatLogger) Infof(format string, params ...any) {
|
2026-01-14 18:30:38 -05:00
|
|
|
f.record(format)
|
|
|
|
|
f.Logger.Infof(format, params...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Warnf logs a warning and records its format.
|
2026-03-05 12:18:05 +01:00
|
|
|
func (f *formatLogger) Warnf(format string, params ...any) {
|
2026-01-14 18:30:38 -05:00
|
|
|
f.record(format)
|
|
|
|
|
f.Logger.Warnf(format, params...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Errorf logs an error and records its format.
|
2026-03-05 12:18:05 +01:00
|
|
|
func (f *formatLogger) Errorf(format string, params ...any) {
|
2026-01-14 18:30:38 -05:00
|
|
|
f.record(format)
|
|
|
|
|
f.Logger.Errorf(format, params...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Criticalf logs a critical message and records its format.
|
2026-03-05 12:18:05 +01:00
|
|
|
func (f *formatLogger) Criticalf(format string, params ...any) {
|
2026-01-14 18:30:38 -05:00
|
|
|
f.record(format)
|
|
|
|
|
f.Logger.Criticalf(format, params...)
|
|
|
|
|
}
|