Skip to content

Commit 6925adc

Browse files
committed
lnwallet/chancloser: account for aux close outputs in initial coop close fee baseline
1 parent 8126b42 commit 6925adc

File tree

3 files changed

+186
-14
lines changed

3 files changed

+186
-14
lines changed

docs/release-notes/release-notes-0.21.0.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
# Bug Fixes
2323

24-
* [Fixed `OpenChannel` with
24+
- [Fixed `OpenChannel` with
2525
`fund_max`](https://github.com/lightningnetwork/lnd/pull/10488) to use the
2626
protocol-level maximum channel size instead of the user-configured
2727
`maxchansize`. The `maxchansize` config option is intended only for limiting
@@ -69,12 +69,16 @@
6969
channel that was only expected to be used for a single message. The erring
7070
goroutine would block on the second send, leading to a deadlock at shutdown.
7171

72-
* [Fixed `lncli unlock` to wait until the wallet is ready to be
72+
- [Fixed `lncli unlock` to wait until the wallet is ready to be
7373
unlocked](https://github.com/lightningnetwork/lnd/pull/10536)
7474
before sending the unlock request. The command now reports wallet state
7575
transitions during startup, avoiding lost unlocks during slow database
7676
initialization.
7777

78+
- [Fixed coop close fee baseline for channels with auxiliary close outputs](https://github.com/lightningnetwork/lnd/pull/10615)
79+
by including extra outputs in initial fee estimation, preventing underpriced
80+
taproot/custom channel cooperative closes from failing mempool acceptance.
81+
7882
# New Features
7983

8084
- Basic Support for [onion messaging forwarding](https://github.com/lightningnetwork/lnd/pull/9868)

lnwallet/chancloser/chancloser.go

Lines changed: 81 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ type ChanCloser struct {
255255
// calcCoopCloseFee computes an "ideal" absolute co-op close fee given the
256256
// delivery scripts of both parties and our ideal fee rate.
257257
func calcCoopCloseFee(chanType channeldb.ChannelType,
258-
localOutput, remoteOutput *wire.TxOut,
258+
localOutput, remoteOutput *wire.TxOut, extraOutputs []*wire.TxOut,
259259
idealFeeRate chainfee.SatPerKWeight) btcutil.Amount {
260260

261261
var weightEstimator input.TxWeightEstimator
@@ -276,6 +276,9 @@ func calcCoopCloseFee(chanType channeldb.ChannelType,
276276
if remoteOutput != nil {
277277
weightEstimator.AddTxOutput(remoteOutput)
278278
}
279+
for _, extraOutput := range extraOutputs {
280+
weightEstimator.AddTxOutput(extraOutput)
281+
}
279282

280283
totalWeight := weightEstimator.Weight()
281284

@@ -295,7 +298,64 @@ func (d *SimpleCoopFeeEstimator) EstimateFee(chanType channeldb.ChannelType,
295298
localTxOut, remoteTxOut *wire.TxOut,
296299
idealFeeRate chainfee.SatPerKWeight) btcutil.Amount {
297300

298-
return calcCoopCloseFee(chanType, localTxOut, remoteTxOut, idealFeeRate)
301+
return calcCoopCloseFee(
302+
chanType, localTxOut, remoteTxOut, nil, idealFeeRate,
303+
)
304+
}
305+
306+
// estimateCloseFee computes a close fee for the given fee rate while taking
307+
// into account any optional auxiliary close outputs.
308+
func (c *ChanCloser) estimateCloseFee(localTxOut, remoteTxOut *wire.TxOut,
309+
feeRate chainfee.SatPerKWeight) (btcutil.Amount, error) {
310+
311+
// Historical behavior uses the default channel type here and doesn't
312+
// differentiate between channel feature variants.
313+
var defaultChanType channeldb.ChannelType
314+
fee := c.cfg.FeeEstimator.EstimateFee(
315+
defaultChanType, localTxOut, remoteTxOut, feeRate,
316+
)
317+
318+
if c.cfg.AuxCloser.IsNone() {
319+
return fee, nil
320+
}
321+
322+
// Aux output selection can depend on CloseFee. After we bump the fee to
323+
// account for extra output weight, the aux closer can return a different
324+
// output set (for example around dust thresholds). Run a second pass so
325+
// the fee and output set are self-consistent, while keeping this bounded.
326+
for range 2 {
327+
auxOutputs, err := c.auxCloseOutputs(fee)
328+
if err != nil {
329+
return 0, err
330+
}
331+
332+
var extraOutputs []*wire.TxOut
333+
auxOutputs.WhenSome(func(outs AuxCloseOutputs) {
334+
extraOutputs = make(
335+
[]*wire.TxOut, 0, len(outs.ExtraCloseOutputs),
336+
)
337+
for _, closeOutput := range outs.ExtraCloseOutputs {
338+
txOut := closeOutput.TxOut
339+
extraOutputs = append(extraOutputs, &txOut)
340+
}
341+
})
342+
343+
if len(extraOutputs) == 0 {
344+
return fee, nil
345+
}
346+
347+
feeWithAuxOutputs := calcCoopCloseFee(
348+
defaultChanType, localTxOut, remoteTxOut,
349+
extraOutputs, feeRate,
350+
)
351+
if feeWithAuxOutputs <= fee {
352+
return fee, nil
353+
}
354+
355+
fee = feeWithAuxOutputs
356+
}
357+
358+
return fee, nil
299359
}
300360

301361
// NewChanCloser creates a new instance of the channel closure given the passed
@@ -327,7 +387,7 @@ func NewChanCloser(cfg ChanCloseCfg, deliveryScript DeliveryAddrWithKey,
327387

328388
// initFeeBaseline computes our ideal fee rate, and also the largest fee we'll
329389
// accept given information about the delivery script of the remote party.
330-
func (c *ChanCloser) initFeeBaseline() {
390+
func (c *ChanCloser) initFeeBaseline() error {
331391
// Depending on if a balance ends up being dust or not, we'll pass a
332392
// nil TxOut into the EstimateFee call which can handle it.
333393
var localTxOut, remoteTxOut *wire.TxOut
@@ -346,18 +406,25 @@ func (c *ChanCloser) initFeeBaseline() {
346406

347407
// Given the target fee-per-kw, we'll compute what our ideal _total_
348408
// fee will be starting at for this fee negotiation.
349-
c.idealFeeSat = c.cfg.FeeEstimator.EstimateFee(
350-
0, localTxOut, remoteTxOut, c.idealFeeRate,
409+
var err error
410+
c.idealFeeSat, err = c.estimateCloseFee(
411+
localTxOut, remoteTxOut, c.idealFeeRate,
351412
)
413+
if err != nil {
414+
return err
415+
}
352416

353417
// When we're the initiator, we'll want to also factor in the highest
354418
// fee we want to pay. This'll either be 3x the ideal fee, or the
355419
// specified explicit max fee.
356420
c.maxFee = c.idealFeeSat * defaultMaxFeeMultiplier
357421
if c.cfg.MaxFee > 0 {
358-
c.maxFee = c.cfg.FeeEstimator.EstimateFee(
359-
0, localTxOut, remoteTxOut, c.cfg.MaxFee,
422+
c.maxFee, err = c.estimateCloseFee(
423+
localTxOut, remoteTxOut, c.cfg.MaxFee,
360424
)
425+
if err != nil {
426+
return err
427+
}
361428
}
362429

363430
// TODO(ziggie): Make sure the ideal fee is not higher than the max fee.
@@ -366,6 +433,8 @@ func (c *ChanCloser) initFeeBaseline() {
366433
chancloserLog.Infof("Ideal fee for closure of ChannelPoint(%v) "+
367434
"is: %v sat (max_fee=%v sat)", c.cfg.Channel.ChannelPoint(),
368435
int64(c.idealFeeSat), int64(c.maxFee))
436+
437+
return nil
369438
}
370439

371440
// initChanShutdown begins the shutdown process by un-registering the channel,
@@ -740,14 +809,17 @@ func (c *ChanCloser) BeginNegotiation() (fn.Option[lnwire.ClosingSigned],
740809
case closeAwaitingFlush:
741810
// Now that we know their desired delivery script, we can
742811
// compute what our max/ideal fee will be.
743-
c.initFeeBaseline()
812+
err := c.initFeeBaseline()
813+
if err != nil {
814+
return noClosingSigned, err
815+
}
744816

745817
// Before continuing, mark the channel as cooperatively closed
746818
// with a nil txn. Even though we haven't negotiated the final
747819
// txn, this guarantees that our listchannels rpc will be
748820
// externally consistent, and reflect that the channel is being
749821
// shutdown by the time the closing request returns.
750-
err := c.cfg.Channel.MarkCoopBroadcasted(
822+
err = c.cfg.Channel.MarkCoopBroadcasted(
751823
nil, c.closer,
752824
)
753825
if err != nil {

lnwallet/chancloser/chancloser_test.go

Lines changed: 99 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/lightningnetwork/lnd/lnutils"
2222
"github.com/lightningnetwork/lnd/lnwallet"
2323
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
24+
wallettypes "github.com/lightningnetwork/lnd/lnwallet/types"
2425
"github.com/lightningnetwork/lnd/lnwire"
2526
"github.com/lightningnetwork/lnd/tlv"
2627
"github.com/stretchr/testify/require"
@@ -300,6 +301,37 @@ func (m *mockMusigSession) ClosingNonce() (*musig2.Nonces, error) {
300301
func (m *mockMusigSession) InitRemoteNonce(nonce *musig2.Nonces) {
301302
}
302303

304+
type mockAuxChanCloser struct {
305+
extraScript []byte
306+
}
307+
308+
func (m *mockAuxChanCloser) ShutdownBlob(
309+
req wallettypes.AuxShutdownReq) (fn.Option[lnwire.CustomRecords], error) {
310+
311+
return fn.None[lnwire.CustomRecords](), nil
312+
}
313+
314+
func (m *mockAuxChanCloser) AuxCloseOutputs(
315+
desc wallettypes.AuxCloseDesc) (fn.Option[AuxCloseOutputs], error) {
316+
317+
closeOutputs := []lnwallet.CloseOutput{{
318+
TxOut: wire.TxOut{
319+
PkScript: m.extraScript,
320+
Value: 0,
321+
},
322+
}}
323+
324+
return fn.Some(AuxCloseOutputs{
325+
ExtraCloseOutputs: closeOutputs,
326+
}), nil
327+
}
328+
329+
func (m *mockAuxChanCloser) FinalizeClose(desc wallettypes.AuxCloseDesc,
330+
closeTx *wire.MsgTx) error {
331+
332+
return nil
333+
}
334+
303335
type mockCoopFeeEstimator struct {
304336
targetFee btcutil.Amount
305337
}
@@ -367,13 +399,77 @@ func TestMaxFeeClamp(t *testing.T) {
367399

368400
// We'll call initFeeBaseline early here since we need
369401
// the populate these internal variables.
370-
chanCloser.initFeeBaseline()
402+
require.NoError(t, chanCloser.initFeeBaseline())
371403

372404
require.Equal(t, test.maxFee, chanCloser.maxFee)
373405
})
374406
}
375407
}
376408

409+
// TestInitFeeBaselineWithAuxCloseOutputs tests that aux close outputs are
410+
// accounted for in the initial fee baseline calculation.
411+
func TestInitFeeBaselineWithAuxCloseOutputs(t *testing.T) {
412+
t.Parallel()
413+
414+
localScript := bytes.Repeat([]byte{0x11}, 34)
415+
remoteScript := bytes.Repeat([]byte{0x22}, 34)
416+
extraScript := bytes.Repeat([]byte{0x33}, 34)
417+
418+
channel := &mockChannel{
419+
initiator: true,
420+
}
421+
422+
newCloser := func(auxCloser fn.Option[AuxChanCloser]) *ChanCloser {
423+
closer := NewChanCloser(
424+
ChanCloseCfg{
425+
Channel: channel,
426+
FeeEstimator: &SimpleCoopFeeEstimator{},
427+
AuxCloser: auxCloser,
428+
},
429+
DeliveryAddrWithKey{
430+
DeliveryAddress: localScript,
431+
},
432+
chainfee.FeePerKwFloor, 0, nil, lntypes.Local,
433+
)
434+
closer.remoteDeliveryScript = remoteScript
435+
436+
return closer
437+
}
438+
439+
closerNoAux := newCloser(fn.None[AuxChanCloser]())
440+
require.NoError(t, closerNoAux.initFeeBaseline())
441+
442+
closerWithAux := newCloser(fn.Some[AuxChanCloser](&mockAuxChanCloser{
443+
extraScript: extraScript,
444+
}))
445+
require.NoError(t, closerWithAux.initFeeBaseline())
446+
447+
localOutput := &wire.TxOut{
448+
PkScript: localScript,
449+
Value: 0,
450+
}
451+
remoteOutput := &wire.TxOut{
452+
PkScript: remoteScript,
453+
Value: 0,
454+
}
455+
extraOutput := &wire.TxOut{
456+
PkScript: extraScript,
457+
Value: 0,
458+
}
459+
460+
expectedFeeNoAux := calcCoopCloseFee(
461+
0, localOutput, remoteOutput, nil, chainfee.FeePerKwFloor,
462+
)
463+
expectedFeeWithAux := calcCoopCloseFee(
464+
0, localOutput, remoteOutput, []*wire.TxOut{extraOutput},
465+
chainfee.FeePerKwFloor,
466+
)
467+
468+
require.Equal(t, expectedFeeNoAux, closerNoAux.idealFeeSat)
469+
require.Equal(t, expectedFeeWithAux, closerWithAux.idealFeeSat)
470+
require.Greater(t, closerWithAux.idealFeeSat, closerNoAux.idealFeeSat)
471+
}
472+
377473
// TestMaxFeeBailOut tests that once the negotiated fee rate rises above our
378474
// maximum fee, we'll return an error and refuse to process a co-op close
379475
// message.
@@ -533,7 +629,7 @@ func TestTaprootFastClose(t *testing.T) {
533629
},
534630
}, DeliveryAddrWithKey{}, idealFee, 0, nil, lntypes.Local,
535631
)
536-
aliceCloser.initFeeBaseline()
632+
require.NoError(t, aliceCloser.initFeeBaseline())
537633

538634
bobCloser := NewChanCloser(
539635
ChanCloseCfg{
@@ -550,7 +646,7 @@ func TestTaprootFastClose(t *testing.T) {
550646
},
551647
}, DeliveryAddrWithKey{}, idealFee, 0, nil, lntypes.Remote,
552648
)
553-
bobCloser.initFeeBaseline()
649+
require.NoError(t, bobCloser.initFeeBaseline())
554650

555651
// With our set up complete, we'll now initialize the shutdown
556652
// procedure kicked off by Alice.

0 commit comments

Comments
 (0)