Skip to content

Commit dab25a4

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

File tree

3 files changed

+188
-14
lines changed

3 files changed

+188
-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: 82 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,65 @@ 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,
323+
// 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
326+
// bounded.
327+
for range 2 {
328+
auxOutputs, err := c.auxCloseOutputs(fee)
329+
if err != nil {
330+
return 0, err
331+
}
332+
333+
var extraOutputs []*wire.TxOut
334+
auxOutputs.WhenSome(func(outs AuxCloseOutputs) {
335+
extraOutputs = make(
336+
[]*wire.TxOut, 0, len(outs.ExtraCloseOutputs),
337+
)
338+
for _, closeOutput := range outs.ExtraCloseOutputs {
339+
txOut := closeOutput.TxOut
340+
extraOutputs = append(extraOutputs, &txOut)
341+
}
342+
})
343+
344+
if len(extraOutputs) == 0 {
345+
return fee, nil
346+
}
347+
348+
feeWithAuxOutputs := calcCoopCloseFee(
349+
defaultChanType, localTxOut, remoteTxOut,
350+
extraOutputs, feeRate,
351+
)
352+
if feeWithAuxOutputs <= fee {
353+
return fee, nil
354+
}
355+
356+
fee = feeWithAuxOutputs
357+
}
358+
359+
return fee, nil
299360
}
300361

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

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

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

353418
// When we're the initiator, we'll want to also factor in the highest
354419
// fee we want to pay. This'll either be 3x the ideal fee, or the
355420
// specified explicit max fee.
356421
c.maxFee = c.idealFeeSat * defaultMaxFeeMultiplier
357422
if c.cfg.MaxFee > 0 {
358-
c.maxFee = c.cfg.FeeEstimator.EstimateFee(
359-
0, localTxOut, remoteTxOut, c.cfg.MaxFee,
423+
c.maxFee, err = c.estimateCloseFee(
424+
localTxOut, remoteTxOut, c.cfg.MaxFee,
360425
)
426+
if err != nil {
427+
return err
428+
}
361429
}
362430

363431
// TODO(ziggie): Make sure the ideal fee is not higher than the max fee.
@@ -366,6 +434,8 @@ func (c *ChanCloser) initFeeBaseline() {
366434
chancloserLog.Infof("Ideal fee for closure of ChannelPoint(%v) "+
367435
"is: %v sat (max_fee=%v sat)", c.cfg.Channel.ChannelPoint(),
368436
int64(c.idealFeeSat), int64(c.maxFee))
437+
438+
return nil
369439
}
370440

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

745818
// Before continuing, mark the channel as cooperatively closed
746819
// with a nil txn. Even though we haven't negotiated the final
747820
// txn, this guarantees that our listchannels rpc will be
748821
// externally consistent, and reflect that the channel is being
749822
// shutdown by the time the closing request returns.
750-
err := c.cfg.Channel.MarkCoopBroadcasted(
823+
err = c.cfg.Channel.MarkCoopBroadcasted(
751824
nil, c.closer,
752825
)
753826
if err != nil {

lnwallet/chancloser/chancloser_test.go

Lines changed: 100 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,38 @@ 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,
310+
) (fn.Option[lnwire.CustomRecords], error) {
311+
312+
return fn.None[lnwire.CustomRecords](), nil
313+
}
314+
315+
func (m *mockAuxChanCloser) AuxCloseOutputs(
316+
desc wallettypes.AuxCloseDesc) (fn.Option[AuxCloseOutputs], error) {
317+
318+
closeOutputs := []lnwallet.CloseOutput{{
319+
TxOut: wire.TxOut{
320+
PkScript: m.extraScript,
321+
Value: 0,
322+
},
323+
}}
324+
325+
return fn.Some(AuxCloseOutputs{
326+
ExtraCloseOutputs: closeOutputs,
327+
}), nil
328+
}
329+
330+
func (m *mockAuxChanCloser) FinalizeClose(desc wallettypes.AuxCloseDesc,
331+
closeTx *wire.MsgTx) error {
332+
333+
return nil
334+
}
335+
303336
type mockCoopFeeEstimator struct {
304337
targetFee btcutil.Amount
305338
}
@@ -367,13 +400,77 @@ func TestMaxFeeClamp(t *testing.T) {
367400

368401
// We'll call initFeeBaseline early here since we need
369402
// the populate these internal variables.
370-
chanCloser.initFeeBaseline()
403+
require.NoError(t, chanCloser.initFeeBaseline())
371404

372405
require.Equal(t, test.maxFee, chanCloser.maxFee)
373406
})
374407
}
375408
}
376409

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

538635
bobCloser := NewChanCloser(
539636
ChanCloseCfg{
@@ -550,7 +647,7 @@ func TestTaprootFastClose(t *testing.T) {
550647
},
551648
}, DeliveryAddrWithKey{}, idealFee, 0, nil, lntypes.Remote,
552649
)
553-
bobCloser.initFeeBaseline()
650+
require.NoError(t, bobCloser.initFeeBaseline())
554651

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

0 commit comments

Comments
 (0)