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/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, }, } 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, AddressParams: 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(), ) }) } } // 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, ) }