Skip to content

Commit d38c5e6

Browse files
committed
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.
1 parent d1b2bff commit d38c5e6

File tree

2 files changed

+129
-34
lines changed

2 files changed

+129
-34
lines changed

lnwallet/channel.go

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8203,6 +8203,8 @@ type chanCloseOpt struct {
82038203
customSequence fn.Option[uint32]
82048204

82058205
customLockTime fn.Option[uint32]
8206+
8207+
customPayer fn.Option[lntypes.ChannelParty]
82068208
}
82078209

82088210
// ChanCloseOpt is a closure type that cen be used to modify the set of default
@@ -8255,6 +8257,15 @@ func WithCustomLockTime(lockTime uint32) ChanCloseOpt {
82558257
}
82568258
}
82578259

8260+
// WithCustomPayer can be used to specify a custom payer for the closing
8261+
// transaction. This overrides the default payer, which is the initiator of the
8262+
// channel.
8263+
func WithCustomPayer(payer lntypes.ChannelParty) ChanCloseOpt {
8264+
return func(opts *chanCloseOpt) {
8265+
opts.customPayer = fn.Some(payer)
8266+
}
8267+
}
8268+
82588269
// CreateCloseProposal is used by both parties in a cooperative channel close
82598270
// workflow to generate proposed close transactions and signatures. This method
82608271
// 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,
82708281
lc.Lock()
82718282
defer lc.Unlock()
82728283

8273-
// If we're already closing the channel, then ignore this request.
8274-
if lc.isClosed {
8275-
return nil, nil, 0, ErrChanClosing
8276-
}
8277-
82788284
opts := defaultCloseOpts()
82798285
for _, optFunc := range closeOpts {
82808286
optFunc(opts)
82818287
}
82828288

8289+
// Unless there's a custom payer (sign of the RBF flow), if we're
8290+
// already closing the channel, then ignore this request.
8291+
if lc.isClosed && opts.customPayer.IsNone() {
8292+
return nil, nil, 0, ErrChanClosing
8293+
}
8294+
82838295
// Get the final balances after subtracting the proposed fee, taking
82848296
// care not to persist the adjusted balance, as the feeRate may change
82858297
// during the channel closing process.
@@ -8289,7 +8301,7 @@ func (lc *LightningChannel) CreateCloseProposal(proposedFee btcutil.Amount,
82898301
lc.channelState.LocalCommitment.LocalBalance.ToSatoshis(),
82908302
lc.channelState.LocalCommitment.RemoteBalance.ToSatoshis(),
82918303
lc.channelState.LocalCommitment.CommitFee,
8292-
fn.None[lntypes.ChannelParty](),
8304+
opts.customPayer,
82938305
)
82948306
if err != nil {
82958307
return nil, nil, 0, err
@@ -8385,25 +8397,25 @@ func (lc *LightningChannel) CompleteCooperativeClose(
83858397
lc.Lock()
83868398
defer lc.Unlock()
83878399

8388-
// If the channel is already closing, then ignore this request.
8389-
if lc.isClosed {
8390-
// TODO(roasbeef): check to ensure no pending payments
8391-
return nil, 0, ErrChanClosing
8392-
}
8393-
83948400
opts := defaultCloseOpts()
83958401
for _, optFunc := range closeOpts {
83968402
optFunc(opts)
83978403
}
83988404

8405+
// Unless there's a custom payer (sign of the RBF flow), if we're
8406+
// already closing the channel, then ignore this request.
8407+
if lc.isClosed && opts.customPayer.IsNone() {
8408+
return nil, 0, ErrChanClosing
8409+
}
8410+
83998411
// Get the final balances after subtracting the proposed fee.
84008412
ourBalance, theirBalance, err := CoopCloseBalance(
84018413
lc.channelState.ChanType, lc.channelState.IsInitiator,
84028414
proposedFee,
84038415
lc.channelState.LocalCommitment.LocalBalance.ToSatoshis(),
84048416
lc.channelState.LocalCommitment.RemoteBalance.ToSatoshis(),
84058417
lc.channelState.LocalCommitment.CommitFee,
8406-
fn.None[lntypes.ChannelParty](),
8418+
opts.customPayer,
84078419
)
84088420
if err != nil {
84098421
return nil, 0, err

lnwallet/channel_test.go

Lines changed: 104 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -770,29 +770,66 @@ func TestCommitHTLCSigCustomRecordSize(t *testing.T) {
770770
}
771771

772772
// TestCooperativeChannelClosure checks that the coop close process finishes
773-
// with an agreement from both parties, and that the final balances of the
774-
// close tx check out.
773+
// with an agreement from both parties, and that the final balances of the close
774+
// tx check out.
775775
func TestCooperativeChannelClosure(t *testing.T) {
776-
t.Run("tweakless", func(t *testing.T) {
777-
testCoopClose(t, &coopCloseTestCase{
778-
chanType: channeldb.SingleFunderTweaklessBit,
779-
})
780-
})
781-
t.Run("anchors", func(t *testing.T) {
782-
testCoopClose(t, &coopCloseTestCase{
783-
chanType: channeldb.SingleFunderTweaklessBit |
784-
channeldb.AnchorOutputsBit,
785-
anchorAmt: AnchorSize * 2,
776+
testCases := []struct {
777+
name string
778+
closeCase coopCloseTestCase
779+
}{
780+
{
781+
name: "tweakless",
782+
closeCase: coopCloseTestCase{
783+
chanType: channeldb.SingleFunderTweaklessBit,
784+
},
785+
},
786+
{
787+
name: "anchors",
788+
closeCase: coopCloseTestCase{
789+
chanType: channeldb.SingleFunderTweaklessBit |
790+
channeldb.AnchorOutputsBit,
791+
anchorAmt: AnchorSize * 2,
792+
},
793+
},
794+
{
795+
name: "anchors local pay",
796+
closeCase: coopCloseTestCase{
797+
chanType: channeldb.SingleFunderTweaklessBit |
798+
channeldb.AnchorOutputsBit,
799+
anchorAmt: AnchorSize * 2,
800+
customPayer: fn.Some(lntypes.Local),
801+
},
802+
},
803+
{
804+
name: "anchors remote pay",
805+
closeCase: coopCloseTestCase{
806+
chanType: channeldb.SingleFunderTweaklessBit |
807+
channeldb.AnchorOutputsBit,
808+
anchorAmt: AnchorSize * 2,
809+
customPayer: fn.Some(lntypes.Remote),
810+
},
811+
},
812+
}
813+
for _, testCase := range testCases {
814+
t.Run(testCase.name, func(t *testing.T) {
815+
testCoopClose(t, testCase.closeCase)
786816
})
787-
})
817+
}
788818
}
789819

790820
type coopCloseTestCase struct {
791821
chanType channeldb.ChannelType
792822
anchorAmt btcutil.Amount
823+
824+
customPayer fn.Option[lntypes.ChannelParty]
825+
}
826+
827+
type closeOpts struct {
828+
aliceOpts []ChanCloseOpt
829+
bobOpts []ChanCloseOpt
793830
}
794831

795-
func testCoopClose(t *testing.T, testCase *coopCloseTestCase) {
832+
func testCoopClose(t *testing.T, testCase coopCloseTestCase) {
796833
t.Parallel()
797834

798835
// Create a test channel which will be used for the duration of this
@@ -813,17 +850,38 @@ func testCoopClose(t *testing.T, testCase *coopCloseTestCase) {
813850
bobChannel.channelState.LocalCommitment.FeePerKw,
814851
)
815852

853+
customPayer := testCase.customPayer
854+
855+
closeOpts := fn.MapOptionZ(
856+
customPayer, func(payer lntypes.ChannelParty) closeOpts {
857+
// If the local party is paying then from Alice's PoV,
858+
// then local party is paying. From Bob's PoV, the
859+
// remote party is paying. If the remote party is, then
860+
// the opposite is true.
861+
return closeOpts{
862+
aliceOpts: []ChanCloseOpt{
863+
WithCustomPayer(payer),
864+
},
865+
bobOpts: []ChanCloseOpt{
866+
WithCustomPayer(payer.CounterParty()),
867+
},
868+
}
869+
},
870+
)
871+
816872
// We'll start with both Alice and Bob creating a new close proposal
817873
// with the same fee.
818874
aliceFee := aliceChannel.CalcFee(aliceFeeRate)
819875
aliceSig, _, _, err := aliceChannel.CreateCloseProposal(
820876
aliceFee, aliceDeliveryScript, bobDeliveryScript,
877+
closeOpts.aliceOpts...,
821878
)
822879
require.NoError(t, err, "unable to create alice coop close proposal")
823880

824881
bobFee := bobChannel.CalcFee(bobFeeRate)
825882
bobSig, _, _, err := bobChannel.CreateCloseProposal(
826883
bobFee, bobDeliveryScript, aliceDeliveryScript,
884+
closeOpts.bobOpts...,
827885
)
828886
require.NoError(t, err, "unable to create bob coop close proposal")
829887

@@ -832,14 +890,14 @@ func testCoopClose(t *testing.T, testCase *coopCloseTestCase) {
832890
// transaction is well formed, and the signatures verify.
833891
aliceCloseTx, bobTxBalance, err := bobChannel.CompleteCooperativeClose(
834892
bobSig, aliceSig, bobDeliveryScript, aliceDeliveryScript,
835-
bobFee,
893+
bobFee, closeOpts.bobOpts...,
836894
)
837895
require.NoError(t, err, "unable to complete alice cooperative close")
838896
bobCloseSha := aliceCloseTx.TxHash()
839897

840898
bobCloseTx, aliceTxBalance, err := aliceChannel.CompleteCooperativeClose(
841899
aliceSig, bobSig, aliceDeliveryScript, bobDeliveryScript,
842-
aliceFee,
900+
aliceFee, closeOpts.aliceOpts...,
843901
)
844902
require.NoError(t, err, "unable to complete bob cooperative close")
845903
aliceCloseSha := bobCloseTx.TxHash()
@@ -848,18 +906,43 @@ func testCoopClose(t *testing.T, testCase *coopCloseTestCase) {
848906
t.Fatalf("alice and bob close transactions don't match: %v", err)
849907
}
850908

851-
// Finally, make sure the final balances are correct from both's
852-
// perspective.
909+
type chanFees struct {
910+
alice btcutil.Amount
911+
bob btcutil.Amount
912+
}
913+
914+
// Compute the closing fees for each party. If not specified, Alice will
915+
// always pay the fees. Otherwise, it depends on who the payer is.
916+
closeFees := fn.MapOption(func(payer lntypes.ChannelParty) chanFees {
917+
var alice, bob btcutil.Amount
918+
919+
switch payer {
920+
case lntypes.Local:
921+
alice = bobFee
922+
bob = 0
923+
case lntypes.Remote:
924+
bob = bobFee
925+
alice = 0
926+
}
927+
928+
return chanFees{
929+
alice: alice,
930+
bob: bob,
931+
}
932+
})(testCase.customPayer).UnwrapOr(chanFees{alice: bobFee})
933+
934+
// Finally, make sure the final balances are correct from both
935+
// perspectives.
853936
aliceBalance := aliceChannel.channelState.LocalCommitment.
854937
LocalBalance.ToSatoshis()
855938

856-
// The commit balance have had the initiator's (Alice) commitfee and
939+
// The commit balance have had the initiator's (Alice) commit fee and
857940
// any anchors subtracted, so add that back to the final expected
858941
// balance. Alice also pays the coop close fee, so that must be
859942
// subtracted.
860943
commitFee := aliceChannel.channelState.LocalCommitment.CommitFee
861944
expBalanceAlice := aliceBalance + commitFee +
862-
testCase.anchorAmt - bobFee
945+
testCase.anchorAmt - closeFees.alice
863946
if aliceTxBalance != expBalanceAlice {
864947
t.Fatalf("expected balance %v got %v", expBalanceAlice,
865948
aliceTxBalance)
@@ -868,7 +951,7 @@ func testCoopClose(t *testing.T, testCase *coopCloseTestCase) {
868951
// Bob is not the initiator, so his final balance should simply be
869952
// equal to the latest commitment balance.
870953
expBalanceBob := bobChannel.channelState.LocalCommitment.
871-
LocalBalance.ToSatoshis()
954+
LocalBalance.ToSatoshis() - closeFees.bob
872955
if bobTxBalance != expBalanceBob {
873956
t.Fatalf("expected bob's balance to be %v got %v",
874957
expBalanceBob, bobTxBalance)

0 commit comments

Comments
 (0)