mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-02-23 14:40:30 +01:00
242 lines
6.4 KiB
Go
242 lines
6.4 KiB
Go
|
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")
|
||
|
}
|
||
|
}
|
||
|
})
|
||
|
}
|