loop/staticaddr/loopin/loopin_test.go
Slyghtning dc42446bae
staticaddr/loopin: use generated change addresses
Create a fresh static change address for fractional loop-ins and persist
its key locator with the selected HTLC outpoint. Recovery reconstructs
the same change output instead of returning funds to the legacy root
address.
2026-08-12 11:00:28 +02:00

265 lines
7.4 KiB
Go

package loopin
import (
"bytes"
"context"
"testing"
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/staticaddr/address"
"github.com/lightninglabs/loop/staticaddr/deposit"
"github.com/lightninglabs/loop/staticaddr/script"
"github.com/lightninglabs/loop/staticaddr/version"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/stretchr/testify/require"
)
// noopSigner is a minimal SignerClient mock that returns dummy signatures
// without blocking on channels.
type noopSigner struct {
lndclient.SignerClient
}
// SignOutputRaw returns dummy 64-byte signatures for each sign descriptor.
func (s *noopSigner) SignOutputRaw(_ context.Context, _ *wire.MsgTx,
descs []*lndclient.SignDescriptor, _ []*wire.TxOut) ([][]byte, error) {
sigs := make([][]byte, len(descs))
for i := range descs {
sigs[i] = make([]byte, 64)
}
return sigs, nil
}
// TestCreateHtlcSweepTxSweepValue verifies that createHtlcSweepTx derives the
// sweep output value from the HTLC output, not the change output. When a change
// output is present, the sweep must reference the HTLC output value.
func TestCreateHtlcSweepTxSweepValue(t *testing.T) {
t.Parallel()
clientKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
serverKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
network := &chaincfg.RegressionNetParams
swapHash := lntypes.Hash{1, 2, 3}
// Create a static address to derive PkScript.
staticAddr, err := newStaticAddress(
clientKey.PubKey(), serverKey.PubKey(), 4032,
)
require.NoError(t, err)
pkScript, err := staticAddr.StaticAddressScript()
require.NoError(t, err)
addrParams := &script.Parameters{
ClientPubkey: clientKey.PubKey(),
ServerPubkey: serverKey.PubKey(),
PkScript: pkScript,
Expiry: 4032,
ProtocolVersion: version.ProtocolVersion_V0,
}
depositValue := btcutil.Amount(500_000)
deposits := []*deposit.Deposit{
{
OutPoint: wire.OutPoint{
Hash: chainhash.Hash{0xaa},
Index: 0,
},
Value: depositValue,
AddressParams: addrParams,
},
}
feeRate := chainfee.SatPerKWeight(253)
maxFeePercentage := 0.2
// SelectedAmount < total triggers a change output.
selectedAmount := btcutil.Amount(300_000)
loopIn := &StaticAddressLoopIn{
SwapHash: swapHash,
HtlcCltvExpiry: 800,
InitiationHeight: 100,
InitiationTime: time.Now(),
ProtocolVersion: version.ProtocolVersion_V0,
ClientPubkey: clientKey.PubKey(),
ServerPubkey: serverKey.PubKey(),
Deposits: deposits,
ChangeAddressParams: addrParams,
HtlcTxFeeRate: feeRate,
SelectedAmount: selectedAmount,
PaymentTimeoutSeconds: 3600,
}
sweepAddr, err := btcutil.NewAddressTaproot(
make([]byte, 32), network,
)
require.NoError(t, err)
signer := &noopSigner{}
// Build the HTLC transaction once. It has two outputs with distinct
// values: the HTLC output and a change output.
htlcTx, err := loopIn.createHtlcTx(network, feeRate, maxFeePercentage)
require.NoError(t, err)
require.Len(t, htlcTx.TxOut, 2, "expected HTLC + change outputs")
// Identify which output is change and which is HTLC.
var htlcIdx int
if bytes.Equal(htlcTx.TxOut[0].PkScript, pkScript) {
htlcIdx = 1
}
htlcValue := htlcTx.TxOut[htlcIdx].Value
changeValue := htlcTx.TxOut[1-htlcIdx].Value
require.NotEqual(t, htlcValue, changeValue,
"HTLC and change values must differ for this test to be "+
"meaningful")
// Call createHtlcSweepTx and verify that the sweep output is derived
// from the HTLC value, not the change.
sweepTx, err := loopIn.createHtlcSweepTx(
t.Context(), signer, sweepAddr, feeRate,
network, uint32(loopIn.HtlcCltvExpiry)+1,
maxFeePercentage,
)
require.NoError(t, err)
require.Len(t, sweepTx.TxOut, 1)
sweepValue := sweepTx.TxOut[0].Value
require.Greater(t, sweepValue, int64(0))
require.LessOrEqual(t, sweepValue, htlcValue,
"sweep value must not exceed HTLC output value")
require.Greater(t, sweepValue, changeValue,
"sweep value should be greater than change "+
"value, confirming it was derived from "+
"the HTLC output")
}
// TestPaymentTimeoutDuration verifies that zero timeout values fall back to the
// default payment timeout duration.
func TestPaymentTimeoutDuration(t *testing.T) {
t.Parallel()
tests := []struct {
name string
paymentTimeoutSeconds uint32
expected time.Duration
}{
{
name: "default",
expected: time.Duration(DefaultPaymentTimeoutSeconds) * time.Second,
},
{
name: "configured",
paymentTimeoutSeconds: 42,
expected: 42 * time.Second,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
loopIn := &StaticAddressLoopIn{
PaymentTimeoutSeconds: test.paymentTimeoutSeconds,
}
require.Equal(
t, test.expected, loopIn.PaymentTimeoutDuration(),
)
})
}
}
// TestCreateHtlcSweepTxUsesConfirmedHtlcOutpoint verifies that timeout sweeps
// spend the actual server-published HTLC tx variant once it has been recorded.
func TestCreateHtlcSweepTxUsesConfirmedHtlcOutpoint(t *testing.T) {
t.Parallel()
clientKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
serverKey, err := btcec.NewPrivateKey()
require.NoError(t, err)
network := &chaincfg.RegressionNetParams
staticAddr, err := newStaticAddress(
clientKey.PubKey(), serverKey.PubKey(), 4032,
)
require.NoError(t, err)
pkScript, err := staticAddr.StaticAddressScript()
require.NoError(t, err)
addrParams := &address.Parameters{
ClientPubkey: clientKey.PubKey(),
ServerPubkey: serverKey.PubKey(),
PkScript: pkScript,
Expiry: 4032,
ProtocolVersion: version.ProtocolVersion_V0,
}
dep := &deposit.Deposit{
OutPoint: wire.OutPoint{
Hash: chainhash.Hash{0xbb},
Index: 0,
},
Value: 500_000,
AddressParams: addrParams,
}
confirmedHtlcHash := chainhash.Hash{0xcc}
confirmedHtlcValue := btcutil.Amount(275_000)
loopIn := &StaticAddressLoopIn{
SwapHash: lntypes.Hash{3, 2, 1},
HtlcCltvExpiry: 800,
ClientPubkey: clientKey.PubKey(),
ServerPubkey: serverKey.PubKey(),
Deposits: []*deposit.Deposit{dep},
HtlcTxFeeRate: chainfee.SatPerKWeight(253),
HtlcTxHash: &confirmedHtlcHash,
HtlcOutputIndex: 2,
HtlcOutputValue: confirmedHtlcValue,
}
sweepAddr, err := btcutil.NewAddressTaproot(make([]byte, 32), network)
require.NoError(t, err)
sweepTx, err := loopIn.createHtlcSweepTx(
t.Context(), &noopSigner{}, sweepAddr,
chainfee.SatPerKWeight(253), network,
uint32(loopIn.HtlcCltvExpiry)+1, 1,
)
require.NoError(t, err)
require.Len(t, sweepTx.TxIn, 1)
require.Equal(
t, wire.OutPoint{
Hash: confirmedHtlcHash,
Index: 2,
}, sweepTx.TxIn[0].PreviousOutPoint,
)
require.Less(t, sweepTx.TxOut[0].Value, int64(confirmedHtlcValue))
require.Greater(t, sweepTx.TxOut[0].Value, int64(0))
}
// newStaticAddress creates a StaticAddress for testing.
func newStaticAddress(clientKey, serverKey *btcec.PublicKey,
csvExpiry int64) (*script.StaticAddress, error) {
return script.NewStaticAddress(
input.MuSig2Version100RC2, csvExpiry, clientKey, serverKey,
)
}