loop/staticaddr/loopin/loopin_test.go
Slyghtning 63d8f5560e
loopin: enforce HTLC output index invariant in sweep tx construction
createHtlcTx always places the HTLC output at index 0 and the change
output (if any) at index 1. Previously, createHtlcSweepTx attempted to
dynamically find the HTLC index but then unconditionally read
TxOut[0].Value, ignoring the computed index.

Replace the dynamic search with a const htlcInputIndex=0 and a fail-fast
check that errors if the layout invariant is ever violated. Add a test
that verifies the sweep value is derived from the HTLC output, not the
change output.
2026-03-09 08:37:48 +01:00

158 lines
4.6 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 := &address.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")
}
// 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,
)
}