lnd/lnwallet/close_test.go

242 lines
6.4 KiB
Go
Raw Normal View History

package lnwallet
import (
"strconv"
"testing"
"github.com/btcsuite/btcd/btcutil"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/fn/v2"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/stretchr/testify/require"
"pgregory.net/rapid"
)
// genValidAmount generates valid bitcoin amounts (non-negative).
func genValidAmount(t *rapid.T, label string) btcutil.Amount {
return btcutil.Amount(
rapid.Int64Range(
100_000, 21_000_000*100_000_000,
).Draw(t, label),
)
}
// genCoopCloseFee generates a reasonable non-zero cooperative close fee.
func genCoopCloseFee(t *rapid.T) btcutil.Amount {
// Generate a fee between 250-10000 sats which is a reasonable range for
// closing transactions
return btcutil.Amount(
rapid.Int64Range(250, 10_000).Draw(t, "coop_close_fee"),
)
}
// genChannelType generates various channel types, ensuring good coverage of
// different channel configurations including anchor outputs and other features.
func genChannelType(t *rapid.T) channeldb.ChannelType {
var chanType channeldb.ChannelType
// For each bit, decide randomly if it should be set.
bits := []channeldb.ChannelType{
channeldb.DualFunderBit,
channeldb.SingleFunderTweaklessBit,
channeldb.NoFundingTxBit,
channeldb.AnchorOutputsBit,
channeldb.FrozenBit,
channeldb.ZeroHtlcTxFeeBit,
channeldb.LeaseExpirationBit,
channeldb.ZeroConfBit,
channeldb.ScidAliasChanBit,
channeldb.ScidAliasFeatureBit,
channeldb.SimpleTaprootFeatureBit,
channeldb.TapscriptRootBit,
}
// Helper to bias towards setting specific bits more frequently.
setBit := func(bit channeldb.ChannelType, probability int) {
bitRange := rapid.IntRange(0, 100)
label := "bit_" + strconv.FormatUint(uint64(bit), 2)
if bitRange.Draw(t, label) < probability {
chanType |= bit
}
}
// We want to ensure good coverage of anchor outputs since they affect
// the balance calculation directly. We'll set the anchor bit with a 50%
// chance.
setBit(channeldb.AnchorOutputsBit, 50)
// For other bits, use varying probabilities to ensure good
// distribution.
for _, bit := range bits {
// The anchor bit was already set above so we can skip it here.
if bit == channeldb.AnchorOutputsBit {
continue
}
// Some bits are related, so we'll make sure we capture that
// dep.
switch bit {
case channeldb.TapscriptRootBit:
// If we have TapscriptRootBit, we must have
// SimpleTaprootFeatureBit.
if chanType&channeldb.SimpleTaprootFeatureBit != 0 {
// 70% chance if taproot is enabled.
setBit(bit, 70)
}
case channeldb.DualFunderBit:
// 40% chance of dual funding.
setBit(bit, 40)
default:
// 30% chance for other bits.
setBit(bit, 30)
}
}
return chanType
}
// genFeePayer generates optional fee payer.
func genFeePayer(t *rapid.T) fn.Option[lntypes.ChannelParty] {
if !rapid.Bool().Draw(t, "has_fee_payer") {
return fn.None[lntypes.ChannelParty]()
}
if rapid.Bool().Draw(t, "is_local") {
return fn.Some(lntypes.Local)
}
return fn.Some(lntypes.Remote)
}
// genCommitFee generates a reasonable non-zero commitment fee.
func genCommitFee(t *rapid.T) btcutil.Amount {
// Generate a reasonable commit fee between 100-5000 sats
return btcutil.Amount(
rapid.Int64Range(100, 5_000).Draw(t, "commit_fee"),
)
}
// TestCoopCloseBalance tests fundamental properties of CoopCloseBalance. This
// ensures that the closing fee is always subtracted from the correct balance,
// amongst other properties.
func TestCoopCloseBalance(tt *testing.T) {
tt.Parallel()
rapid.Check(tt, func(t *rapid.T) {
require := require.New(t)
// Generate test inputs
chanType := genChannelType(t)
isInitiator := rapid.Bool().Draw(t, "is_initiator")
// Generate amounts using specific generators
coopCloseFee := genCoopCloseFee(t)
ourBalance := genValidAmount(t, "local balance")
theirBalance := genValidAmount(t, "remote balance")
feePayer := genFeePayer(t)
commitFee := genCommitFee(t)
ourFinal, theirFinal, err := CoopCloseBalance(
chanType, isInitiator, coopCloseFee,
ourBalance, theirBalance, commitFee, feePayer,
)
// Property 1: If inputs are non-negative, we either get valid
// outputs or an error.
if err != nil {
// On error, final balances should be 0
require.Zero(
ourFinal,
"expected zero our_balance on error",
)
require.Zero(
theirFinal,
"expected zero their_balance on error",
)
return
}
// Property 2: Final balances should be non-negative.
require.GreaterOrEqual(
ourFinal, btcutil.Amount(0),
"our final balance should be non-negative",
)
require.GreaterOrEqual(
theirFinal, btcutil.Amount(0),
"their final balance should be non-negative",
)
// Property 3: Total balance should be conserved minus fees.
initialTotal := ourBalance + theirBalance
initialTotal += commitFee
if chanType.HasAnchors() {
initialTotal += 2 * AnchorSize
}
finalTotal := ourFinal + theirFinal + coopCloseFee
require.Equal(
initialTotal, finalTotal,
"total balance should be conserved",
)
// Property 4: When feePayer is specified, that party's balance
// should be reduced by exactly the coopCloseFee.
if feePayer.IsSome() {
payer := feePayer.UnwrapOrFail(tt)
if payer == lntypes.Local {
require.LessOrEqual(
ourBalance-(ourFinal+coopCloseFee),
btcutil.Amount(0),
"local balance reduced by more than fee", //nolint:ll
)
} else {
require.LessOrEqual(
theirBalance-(theirFinal+coopCloseFee),
btcutil.Amount(0),
"remote balance reduced by more than fee", //nolint:ll
)
}
}
// Property 5: For anchor channels, verify the correct final
// balance factors in the anchor amount.
if chanType.HasAnchors() {
// The initiator delta is the commit fee plus anchor
// amount.
initiatorDelta := commitFee + 2*AnchorSize
// Default to initiator paying unless explicitly
// specified.
isLocalPaying := isInitiator
if feePayer.IsSome() {
isLocalPaying = feePayer.UnwrapOrFail(tt) ==
lntypes.Local
}
if isInitiator {
expectedBalance := ourBalance + initiatorDelta
if isLocalPaying {
expectedBalance -= coopCloseFee
}
require.Equal(expectedBalance, ourFinal,
"initiator (local) balance incorrect")
} else {
// They are the initiator
expectedBalance := theirBalance + initiatorDelta
if !isLocalPaying {
expectedBalance -= coopCloseFee
}
require.Equal(expectedBalance, theirFinal,
"initiator (remote) balance incorrect")
}
}
})
}