mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-03-13 11:09:23 +01:00
lnwallet: update CoopCloseBalance to allow a paying party
This preps us for an upcoming change to the rbf coop state machine where either party can pay for the channel fees. We also add a new test to make sure the new function adheres to some key properties.
This commit is contained in:
parent
b8cf5ae98f
commit
d1b2bff2c8
3 changed files with 268 additions and 6 deletions
|
@ -8289,6 +8289,7 @@ func (lc *LightningChannel) CreateCloseProposal(proposedFee btcutil.Amount,
|
||||||
lc.channelState.LocalCommitment.LocalBalance.ToSatoshis(),
|
lc.channelState.LocalCommitment.LocalBalance.ToSatoshis(),
|
||||||
lc.channelState.LocalCommitment.RemoteBalance.ToSatoshis(),
|
lc.channelState.LocalCommitment.RemoteBalance.ToSatoshis(),
|
||||||
lc.channelState.LocalCommitment.CommitFee,
|
lc.channelState.LocalCommitment.CommitFee,
|
||||||
|
fn.None[lntypes.ChannelParty](),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, 0, err
|
return nil, nil, 0, err
|
||||||
|
@ -8402,6 +8403,7 @@ func (lc *LightningChannel) CompleteCooperativeClose(
|
||||||
lc.channelState.LocalCommitment.LocalBalance.ToSatoshis(),
|
lc.channelState.LocalCommitment.LocalBalance.ToSatoshis(),
|
||||||
lc.channelState.LocalCommitment.RemoteBalance.ToSatoshis(),
|
lc.channelState.LocalCommitment.RemoteBalance.ToSatoshis(),
|
||||||
lc.channelState.LocalCommitment.CommitFee,
|
lc.channelState.LocalCommitment.CommitFee,
|
||||||
|
fn.None[lntypes.ChannelParty](),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
|
|
241
lnwallet/close_test.go
Normal file
241
lnwallet/close_test.go
Normal file
|
@ -0,0 +1,241 @@
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -1033,8 +1033,9 @@ func CreateCommitTx(chanType channeldb.ChannelType,
|
||||||
// CoopCloseBalance returns the final balances that should be used to create
|
// CoopCloseBalance returns the final balances that should be used to create
|
||||||
// the cooperative close tx, given the channel type and transaction fee.
|
// the cooperative close tx, given the channel type and transaction fee.
|
||||||
func CoopCloseBalance(chanType channeldb.ChannelType, isInitiator bool,
|
func CoopCloseBalance(chanType channeldb.ChannelType, isInitiator bool,
|
||||||
coopCloseFee, ourBalance, theirBalance,
|
coopCloseFee, ourBalance, theirBalance, commitFee btcutil.Amount,
|
||||||
commitFee btcutil.Amount) (btcutil.Amount, btcutil.Amount, error) {
|
feePayer fn.Option[lntypes.ChannelParty],
|
||||||
|
) (btcutil.Amount, btcutil.Amount, error) {
|
||||||
|
|
||||||
// We'll make sure we account for the complete balance by adding the
|
// We'll make sure we account for the complete balance by adding the
|
||||||
// current dangling commitment fee to the balance of the initiator.
|
// current dangling commitment fee to the balance of the initiator.
|
||||||
|
@ -1046,16 +1047,34 @@ func CoopCloseBalance(chanType channeldb.ChannelType, isInitiator bool,
|
||||||
initiatorDelta += 2 * AnchorSize
|
initiatorDelta += 2 * AnchorSize
|
||||||
}
|
}
|
||||||
|
|
||||||
// The initiator will pay the full coop close fee, subtract that value
|
// To start with, we'll add the anchor and/or commitment fee to the
|
||||||
// from their balance.
|
// balance of the initiator.
|
||||||
initiatorDelta -= coopCloseFee
|
|
||||||
|
|
||||||
if isInitiator {
|
if isInitiator {
|
||||||
ourBalance += initiatorDelta
|
ourBalance += initiatorDelta
|
||||||
} else {
|
} else {
|
||||||
theirBalance += initiatorDelta
|
theirBalance += initiatorDelta
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// With the initiator's balance credited, we'll now subtract the closing
|
||||||
|
// fee from the closing party. By default, the initiator pays the full
|
||||||
|
// amount, but this can be overridden by the feePayer option.
|
||||||
|
defaultPayer := func() lntypes.ChannelParty {
|
||||||
|
if isInitiator {
|
||||||
|
return lntypes.Local
|
||||||
|
}
|
||||||
|
|
||||||
|
return lntypes.Remote
|
||||||
|
}()
|
||||||
|
payer := feePayer.UnwrapOr(defaultPayer)
|
||||||
|
|
||||||
|
// Based on the payer computed above, we'll subtract the closing fee.
|
||||||
|
switch payer {
|
||||||
|
case lntypes.Local:
|
||||||
|
ourBalance -= coopCloseFee
|
||||||
|
case lntypes.Remote:
|
||||||
|
theirBalance -= coopCloseFee
|
||||||
|
}
|
||||||
|
|
||||||
// During fee negotiation it should always be verified that the
|
// During fee negotiation it should always be verified that the
|
||||||
// initiator can pay the proposed fee, but we do a sanity check just to
|
// initiator can pay the proposed fee, but we do a sanity check just to
|
||||||
// be sure here.
|
// be sure here.
|
||||||
|
|
Loading…
Add table
Reference in a new issue