mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
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.
265 lines
7.4 KiB
Go
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,
|
|
)
|
|
}
|