mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-01-19 05:45:21 +01:00
lnwallet: update core coop close logic with custom payer
In this commit, we update the core coop close logic with the new custom payer param. We also expand the existing unit tests to ensure that the fee is deducted from the proper party.
This commit is contained in:
parent
d1b2bff2c8
commit
d38c5e6222
@ -8203,6 +8203,8 @@ type chanCloseOpt struct {
|
||||
customSequence fn.Option[uint32]
|
||||
|
||||
customLockTime fn.Option[uint32]
|
||||
|
||||
customPayer fn.Option[lntypes.ChannelParty]
|
||||
}
|
||||
|
||||
// ChanCloseOpt is a closure type that cen be used to modify the set of default
|
||||
@ -8255,6 +8257,15 @@ func WithCustomLockTime(lockTime uint32) ChanCloseOpt {
|
||||
}
|
||||
}
|
||||
|
||||
// WithCustomPayer can be used to specify a custom payer for the closing
|
||||
// transaction. This overrides the default payer, which is the initiator of the
|
||||
// channel.
|
||||
func WithCustomPayer(payer lntypes.ChannelParty) ChanCloseOpt {
|
||||
return func(opts *chanCloseOpt) {
|
||||
opts.customPayer = fn.Some(payer)
|
||||
}
|
||||
}
|
||||
|
||||
// CreateCloseProposal is used by both parties in a cooperative channel close
|
||||
// workflow to generate proposed close transactions and signatures. This method
|
||||
// should only be executed once all pending HTLCs (if any) on the channel have
|
||||
@ -8270,16 +8281,17 @@ func (lc *LightningChannel) CreateCloseProposal(proposedFee btcutil.Amount,
|
||||
lc.Lock()
|
||||
defer lc.Unlock()
|
||||
|
||||
// If we're already closing the channel, then ignore this request.
|
||||
if lc.isClosed {
|
||||
return nil, nil, 0, ErrChanClosing
|
||||
}
|
||||
|
||||
opts := defaultCloseOpts()
|
||||
for _, optFunc := range closeOpts {
|
||||
optFunc(opts)
|
||||
}
|
||||
|
||||
// Unless there's a custom payer (sign of the RBF flow), if we're
|
||||
// already closing the channel, then ignore this request.
|
||||
if lc.isClosed && opts.customPayer.IsNone() {
|
||||
return nil, nil, 0, ErrChanClosing
|
||||
}
|
||||
|
||||
// Get the final balances after subtracting the proposed fee, taking
|
||||
// care not to persist the adjusted balance, as the feeRate may change
|
||||
// during the channel closing process.
|
||||
@ -8289,7 +8301,7 @@ func (lc *LightningChannel) CreateCloseProposal(proposedFee btcutil.Amount,
|
||||
lc.channelState.LocalCommitment.LocalBalance.ToSatoshis(),
|
||||
lc.channelState.LocalCommitment.RemoteBalance.ToSatoshis(),
|
||||
lc.channelState.LocalCommitment.CommitFee,
|
||||
fn.None[lntypes.ChannelParty](),
|
||||
opts.customPayer,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
@ -8385,17 +8397,17 @@ func (lc *LightningChannel) CompleteCooperativeClose(
|
||||
lc.Lock()
|
||||
defer lc.Unlock()
|
||||
|
||||
// If the channel is already closing, then ignore this request.
|
||||
if lc.isClosed {
|
||||
// TODO(roasbeef): check to ensure no pending payments
|
||||
return nil, 0, ErrChanClosing
|
||||
}
|
||||
|
||||
opts := defaultCloseOpts()
|
||||
for _, optFunc := range closeOpts {
|
||||
optFunc(opts)
|
||||
}
|
||||
|
||||
// Unless there's a custom payer (sign of the RBF flow), if we're
|
||||
// already closing the channel, then ignore this request.
|
||||
if lc.isClosed && opts.customPayer.IsNone() {
|
||||
return nil, 0, ErrChanClosing
|
||||
}
|
||||
|
||||
// Get the final balances after subtracting the proposed fee.
|
||||
ourBalance, theirBalance, err := CoopCloseBalance(
|
||||
lc.channelState.ChanType, lc.channelState.IsInitiator,
|
||||
@ -8403,7 +8415,7 @@ func (lc *LightningChannel) CompleteCooperativeClose(
|
||||
lc.channelState.LocalCommitment.LocalBalance.ToSatoshis(),
|
||||
lc.channelState.LocalCommitment.RemoteBalance.ToSatoshis(),
|
||||
lc.channelState.LocalCommitment.CommitFee,
|
||||
fn.None[lntypes.ChannelParty](),
|
||||
opts.customPayer,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
|
@ -770,29 +770,66 @@ func TestCommitHTLCSigCustomRecordSize(t *testing.T) {
|
||||
}
|
||||
|
||||
// TestCooperativeChannelClosure checks that the coop close process finishes
|
||||
// with an agreement from both parties, and that the final balances of the
|
||||
// close tx check out.
|
||||
// with an agreement from both parties, and that the final balances of the close
|
||||
// tx check out.
|
||||
func TestCooperativeChannelClosure(t *testing.T) {
|
||||
t.Run("tweakless", func(t *testing.T) {
|
||||
testCoopClose(t, &coopCloseTestCase{
|
||||
chanType: channeldb.SingleFunderTweaklessBit,
|
||||
testCases := []struct {
|
||||
name string
|
||||
closeCase coopCloseTestCase
|
||||
}{
|
||||
{
|
||||
name: "tweakless",
|
||||
closeCase: coopCloseTestCase{
|
||||
chanType: channeldb.SingleFunderTweaklessBit,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "anchors",
|
||||
closeCase: coopCloseTestCase{
|
||||
chanType: channeldb.SingleFunderTweaklessBit |
|
||||
channeldb.AnchorOutputsBit,
|
||||
anchorAmt: AnchorSize * 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "anchors local pay",
|
||||
closeCase: coopCloseTestCase{
|
||||
chanType: channeldb.SingleFunderTweaklessBit |
|
||||
channeldb.AnchorOutputsBit,
|
||||
anchorAmt: AnchorSize * 2,
|
||||
customPayer: fn.Some(lntypes.Local),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "anchors remote pay",
|
||||
closeCase: coopCloseTestCase{
|
||||
chanType: channeldb.SingleFunderTweaklessBit |
|
||||
channeldb.AnchorOutputsBit,
|
||||
anchorAmt: AnchorSize * 2,
|
||||
customPayer: fn.Some(lntypes.Remote),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
testCoopClose(t, testCase.closeCase)
|
||||
})
|
||||
})
|
||||
t.Run("anchors", func(t *testing.T) {
|
||||
testCoopClose(t, &coopCloseTestCase{
|
||||
chanType: channeldb.SingleFunderTweaklessBit |
|
||||
channeldb.AnchorOutputsBit,
|
||||
anchorAmt: AnchorSize * 2,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type coopCloseTestCase struct {
|
||||
chanType channeldb.ChannelType
|
||||
anchorAmt btcutil.Amount
|
||||
|
||||
customPayer fn.Option[lntypes.ChannelParty]
|
||||
}
|
||||
|
||||
func testCoopClose(t *testing.T, testCase *coopCloseTestCase) {
|
||||
type closeOpts struct {
|
||||
aliceOpts []ChanCloseOpt
|
||||
bobOpts []ChanCloseOpt
|
||||
}
|
||||
|
||||
func testCoopClose(t *testing.T, testCase coopCloseTestCase) {
|
||||
t.Parallel()
|
||||
|
||||
// Create a test channel which will be used for the duration of this
|
||||
@ -813,17 +850,38 @@ func testCoopClose(t *testing.T, testCase *coopCloseTestCase) {
|
||||
bobChannel.channelState.LocalCommitment.FeePerKw,
|
||||
)
|
||||
|
||||
customPayer := testCase.customPayer
|
||||
|
||||
closeOpts := fn.MapOptionZ(
|
||||
customPayer, func(payer lntypes.ChannelParty) closeOpts {
|
||||
// If the local party is paying then from Alice's PoV,
|
||||
// then local party is paying. From Bob's PoV, the
|
||||
// remote party is paying. If the remote party is, then
|
||||
// the opposite is true.
|
||||
return closeOpts{
|
||||
aliceOpts: []ChanCloseOpt{
|
||||
WithCustomPayer(payer),
|
||||
},
|
||||
bobOpts: []ChanCloseOpt{
|
||||
WithCustomPayer(payer.CounterParty()),
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// We'll start with both Alice and Bob creating a new close proposal
|
||||
// with the same fee.
|
||||
aliceFee := aliceChannel.CalcFee(aliceFeeRate)
|
||||
aliceSig, _, _, err := aliceChannel.CreateCloseProposal(
|
||||
aliceFee, aliceDeliveryScript, bobDeliveryScript,
|
||||
closeOpts.aliceOpts...,
|
||||
)
|
||||
require.NoError(t, err, "unable to create alice coop close proposal")
|
||||
|
||||
bobFee := bobChannel.CalcFee(bobFeeRate)
|
||||
bobSig, _, _, err := bobChannel.CreateCloseProposal(
|
||||
bobFee, bobDeliveryScript, aliceDeliveryScript,
|
||||
closeOpts.bobOpts...,
|
||||
)
|
||||
require.NoError(t, err, "unable to create bob coop close proposal")
|
||||
|
||||
@ -832,14 +890,14 @@ func testCoopClose(t *testing.T, testCase *coopCloseTestCase) {
|
||||
// transaction is well formed, and the signatures verify.
|
||||
aliceCloseTx, bobTxBalance, err := bobChannel.CompleteCooperativeClose(
|
||||
bobSig, aliceSig, bobDeliveryScript, aliceDeliveryScript,
|
||||
bobFee,
|
||||
bobFee, closeOpts.bobOpts...,
|
||||
)
|
||||
require.NoError(t, err, "unable to complete alice cooperative close")
|
||||
bobCloseSha := aliceCloseTx.TxHash()
|
||||
|
||||
bobCloseTx, aliceTxBalance, err := aliceChannel.CompleteCooperativeClose(
|
||||
aliceSig, bobSig, aliceDeliveryScript, bobDeliveryScript,
|
||||
aliceFee,
|
||||
aliceFee, closeOpts.aliceOpts...,
|
||||
)
|
||||
require.NoError(t, err, "unable to complete bob cooperative close")
|
||||
aliceCloseSha := bobCloseTx.TxHash()
|
||||
@ -848,18 +906,43 @@ func testCoopClose(t *testing.T, testCase *coopCloseTestCase) {
|
||||
t.Fatalf("alice and bob close transactions don't match: %v", err)
|
||||
}
|
||||
|
||||
// Finally, make sure the final balances are correct from both's
|
||||
// perspective.
|
||||
type chanFees struct {
|
||||
alice btcutil.Amount
|
||||
bob btcutil.Amount
|
||||
}
|
||||
|
||||
// Compute the closing fees for each party. If not specified, Alice will
|
||||
// always pay the fees. Otherwise, it depends on who the payer is.
|
||||
closeFees := fn.MapOption(func(payer lntypes.ChannelParty) chanFees {
|
||||
var alice, bob btcutil.Amount
|
||||
|
||||
switch payer {
|
||||
case lntypes.Local:
|
||||
alice = bobFee
|
||||
bob = 0
|
||||
case lntypes.Remote:
|
||||
bob = bobFee
|
||||
alice = 0
|
||||
}
|
||||
|
||||
return chanFees{
|
||||
alice: alice,
|
||||
bob: bob,
|
||||
}
|
||||
})(testCase.customPayer).UnwrapOr(chanFees{alice: bobFee})
|
||||
|
||||
// Finally, make sure the final balances are correct from both
|
||||
// perspectives.
|
||||
aliceBalance := aliceChannel.channelState.LocalCommitment.
|
||||
LocalBalance.ToSatoshis()
|
||||
|
||||
// The commit balance have had the initiator's (Alice) commitfee and
|
||||
// The commit balance have had the initiator's (Alice) commit fee and
|
||||
// any anchors subtracted, so add that back to the final expected
|
||||
// balance. Alice also pays the coop close fee, so that must be
|
||||
// subtracted.
|
||||
commitFee := aliceChannel.channelState.LocalCommitment.CommitFee
|
||||
expBalanceAlice := aliceBalance + commitFee +
|
||||
testCase.anchorAmt - bobFee
|
||||
testCase.anchorAmt - closeFees.alice
|
||||
if aliceTxBalance != expBalanceAlice {
|
||||
t.Fatalf("expected balance %v got %v", expBalanceAlice,
|
||||
aliceTxBalance)
|
||||
@ -868,7 +951,7 @@ func testCoopClose(t *testing.T, testCase *coopCloseTestCase) {
|
||||
// Bob is not the initiator, so his final balance should simply be
|
||||
// equal to the latest commitment balance.
|
||||
expBalanceBob := bobChannel.channelState.LocalCommitment.
|
||||
LocalBalance.ToSatoshis()
|
||||
LocalBalance.ToSatoshis() - closeFees.bob
|
||||
if bobTxBalance != expBalanceBob {
|
||||
t.Fatalf("expected bob's balance to be %v got %v",
|
||||
expBalanceBob, bobTxBalance)
|
||||
|
Loading…
Reference in New Issue
Block a user