Merge pull request #10969 from darioAnongba/fix/chancloser-include-aux-outputs
What changed, and why it matters
This patch fixes a bug in LND's cooperative channel-closing logic where extra 'auxiliary' outputs (used by custom/taproot channel types) were not counted when the initial closing fee was first estimated. If the fee was too low, the resulting close transaction could be rejected by the Bitcoin network's mempool. The fix adds a new 'shape' query so the closer knows the size of these extra outputs up front, includes them in the fee calculation, and then verifies that the final outputs match the declared shape before signing.
Review and merge if not already merged; ensure any custom channel implementations (e.g., taproot assets) correctly implement AuxCloseShape and return stable shapes independent of fee. Monitor for follow-up patches that extend RBF coop close paths to also use aux shapes.
Security signals we found
Underpriced transaction fee leading to mempool rejection / DoS of cooperative close
Fee estimation did not include auxiliary output weight
Added validation that concrete auxiliary outputs match declared shape before signing
Potential fee negotiation mismatch between parties if aux output shape changes
Evidence from the diff
The change extends the ChanCloser fee-baseline calculation to account for auxiliary close outputs. A new AuxCloseShape/AuxCloseOutputShape type is introduced, the AuxChanCloser interface gains an AuxCloseShape method, and calcCoopCloseFee/EstimateFee now accept extraTxOuts. initFeeBaseline fetches the shape, synthesizes zero-value TxOuts of the correct pkScript size for weight estimation, and stores the shape. Before signing, validateAuxShape checks that concrete ExtraCloseOutputs match the declared count, ownership, and pkScript size (order-agnostic). RBF coop transitions pass nil for now. Tests cover fee increase with aux outputs and shape validation cases.
Changed components
lnwallet/chancloser/chancloser.golnwallet/chancloser/aux_closer.golnwallet/chancloser/interface.golnwallet/chancloser/rbf_coop_transitions.golnwallet/types/close_types.goInspect captured patch +398 / −31
### docs/release-notes/release-notes-0.21.3.md
@@ -62,6 +62,10 @@
transaction confirmed stayed in the `channelReadySent` opening state forever,
never added to the graph and never announced. Only a restart recovered.
+* [Fixed coop close fee baseline for channels with auxiliary close outputs](https://github.com/lightningnetwork/lnd/pull/10969)
+ by including extra outputs in initial fee estimation, preventing underpriced
+ taproot/custom channel cooperative closes from failing mempool acceptance.
+
# New Features
## Functional Enhancements
### lnwallet/chancloser/aux_closer.go
@@ -21,6 +21,26 @@ type AuxCloseOutputs struct {
CustomSort lnwallet.CloseSortFunc
}
+// AuxCloseOutputShape describes a single auxiliary close output in a
+// fee-independent way, capturing only the properties that contribute to the
+// weight of the co-op close transaction.
+type AuxCloseOutputShape struct {
+ // IsLocal is true if the output belongs to the local party.
+ IsLocal bool
+
+ // PkScriptSize is the size, in bytes, of the output's pkScript.
+ PkScriptSize int
+}
+
+// AuxCloseShape is the fee-independent shape of the auxiliary outputs that
+// will be added to the co-op close transaction: their number and pkScript
+// sizes. The values of the concrete outputs may still depend on the
+// negotiated close fee, but values don't contribute to transaction weight.
+type AuxCloseShape struct {
+ // Outputs describes each auxiliary close output.
+ Outputs []AuxCloseOutputShape
+}
+
// AuxChanCloser is used to allow an external caller to modify the co-op close
// transaction.
type AuxChanCloser interface {
@@ -29,6 +49,14 @@ type AuxChanCloser interface {
ShutdownBlob(req types.AuxShutdownReq) (fn.Option[lnwire.CustomRecords],
error)
+ // AuxCloseShape returns the fee-independent shape of the auxiliary
+ // outputs required to close the channel. The shape determines the
+ // transaction weight used for fee negotiation, so it MUST NOT depend
+ // on the close fee, while the values of the concrete outputs returned
+ // by AuxCloseOutputs may.
+ AuxCloseShape(desc types.AuxCloseShapeDesc) (fn.Option[AuxCloseShape],
+ error)
+
// AuxCloseOutputs returns the set of custom outputs that should be used
// to construct the co-op close transaction.
AuxCloseOutputs(desc types.AuxCloseDesc) (fn.Option[AuxCloseOutputs],
### lnwallet/chancloser/chancloser.go
@@ -57,6 +57,11 @@ var (
// errNoShutdownNonce is returned when a shutdown message is received
// w/o a nonce for a taproot channel.
errNoShutdownNonce = fmt.Errorf("shutdown nonce not populated")
+
+ // ErrAuxShapeMismatch is returned when the concrete auxiliary close
+ // outputs don't match the shape that was declared for fee estimation.
+ ErrAuxShapeMismatch = fmt.Errorf("aux close outputs don't match " +
+ "declared shape")
)
// closeState represents all the possible states the channel closer state
@@ -257,12 +262,18 @@ type ChanCloser struct {
// auxOutputs are the optional additional outputs that might be added to
// the closing transaction.
auxOutputs fn.Option[AuxCloseOutputs]
+
+ // auxShape is the fee-independent shape of the auxiliary close
+ // outputs, as declared by the aux closer when the fee baseline was
+ // established. The concrete outputs are validated against it.
+ auxShape fn.Option[AuxCloseShape]
}
// calcCoopCloseFee computes an "ideal" absolute co-op close fee given the
-// delivery scripts of both parties and our ideal fee rate.
+// delivery scripts of both parties, any extra (e.g. auxiliary) close outputs
+// to include in the weight estimate, and our ideal fee rate.
func calcCoopCloseFee(chanType channeldb.ChannelType,
- localOutput, remoteOutput *wire.TxOut,
+ localOutput, remoteOutput *wire.TxOut, extraOutputs []*wire.TxOut,
idealFeeRate chainfee.SatPerKWeight) btcutil.Amount {
var weightEstimator input.TxWeightEstimator
@@ -283,6 +294,9 @@ func calcCoopCloseFee(chanType channeldb.ChannelType,
if remoteOutput != nil {
weightEstimator.AddTxOutput(remoteOutput)
}
+ for _, extraOutput := range extraOutputs {
+ weightEstimator.AddTxOutput(extraOutput)
+ }
totalWeight := weightEstimator.Weight()
@@ -296,13 +310,78 @@ type SimpleCoopFeeEstimator struct {
}
// EstimateFee estimates an _absolute_ fee for a co-op close transaction given
-// the local+remote tx outs (for the co-op close transaction), channel type,
-// and ideal fee rate.
+// the local+remote tx outs (for the co-op close transaction), any extra
+// outputs the close transaction will carry, channel type, and ideal fee rate.
func (d *SimpleCoopFeeEstimator) EstimateFee(chanType channeldb.ChannelType,
- localTxOut, remoteTxOut *wire.TxOut,
+ localTxOut, remoteTxOut *wire.TxOut, extraTxOuts []*wire.TxOut,
idealFeeRate chainfee.SatPerKWeight) btcutil.Amount {
- return calcCoopCloseFee(chanType, localTxOut, remoteTxOut, idealFeeRate)
+ return calcCoopCloseFee(
+ chanType, localTxOut, remoteTxOut, extraTxOuts, idealFeeRate,
+ )
+}
+
+// auxShapeTxOuts synthesizes zero-value outputs matching the declared aux
+// close shape, for use in fee estimation. Only the pkScript sizes matter, as
+// output values don't contribute to transaction weight.
+func auxShapeTxOuts(shape fn.Option[AuxCloseShape]) []*wire.TxOut {
+ var txOuts []*wire.TxOut
+ shape.WhenSome(func(s AuxCloseShape) {
+ txOuts = make([]*wire.TxOut, 0, len(s.Outputs))
+ for _, out := range s.Outputs {
+ txOuts = append(txOuts, &wire.TxOut{
+ PkScript: make([]byte, out.PkScriptSize),
+ })
+ }
+ })
+
+ return txOuts
+}
+
+// validateAuxShape ensures the concrete auxiliary outputs match the shape
+// that was declared for fee estimation. A mismatch would invalidate the
+// negotiated fee, so we fail rather than sign off on a misestimated close
+// transaction.
+func validateAuxShape(shape fn.Option[AuxCloseShape],
+ auxOutputs fn.Option[AuxCloseOutputs]) error {
+
+ var declared []AuxCloseOutputShape
+ shape.WhenSome(func(s AuxCloseShape) {
+ declared = s.Outputs
+ })
+
+ var concrete []lnwallet.CloseOutput
+ auxOutputs.WhenSome(func(outs AuxCloseOutputs) {
+ concrete = outs.ExtraCloseOutputs
+ })
+
+ if len(declared) != len(concrete) {
+ return fmt.Errorf("%w: declared %v aux close outputs, got %v",
+ ErrAuxShapeMismatch, len(declared), len(concrete))
+ }
+
+ // Compare the two sets while being agnostic to ordering: count the
+ // declared outputs per kind, then match each concrete output against
+ // the remaining declared kinds.
+ kinds := make(map[AuxCloseOutputShape]int, len(declared))
+ for _, out := range declared {
+ kinds[out]++
+ }
+ for _, out := range concrete {
+ kind := AuxCloseOutputShape{
+ IsLocal: out.IsLocal,
+ PkScriptSize: len(out.TxOut.PkScript),
+ }
+ if kinds[kind] == 0 {
+ return fmt.Errorf("%w: output (is_local=%v, "+
+ "pk_script_size=%v) wasn't declared",
+ ErrAuxShapeMismatch, kind.IsLocal,
+ kind.PkScriptSize)
+ }
+ kinds[kind]--
+ }
+
+ return nil
}
// NewChanCloser creates a new instance of the channel closure given the passed
@@ -333,8 +412,10 @@ func NewChanCloser(cfg ChanCloseCfg, deliveryScript DeliveryAddrWithKey,
}
// initFeeBaseline computes our ideal fee rate, and also the largest fee we'll
-// accept given information about the delivery script of the remote party.
-func (c *ChanCloser) initFeeBaseline() {
+// accept given information about the delivery script of the remote party. It
+// returns an error if the auxiliary close outputs cannot be enumerated to
+// include in the fee estimate.
+func (c *ChanCloser) initFeeBaseline() error {
// Depending on if a balance ends up being dust or not, we'll pass a
// nil TxOut into the EstimateFee call which can handle it.
var localTxOut, remoteTxOut *wire.TxOut
@@ -351,10 +432,20 @@ func (c *ChanCloser) initFeeBaseline() {
}
}
+ // Fetch the fee-independent shape of any auxiliary close outputs, and
+ // synthesize their weight contribution, so the fee we negotiate
+ // matches the final transaction that will carry them.
+ var err error
+ c.auxShape, err = c.auxCloseShape()
+ if err != nil {
+ return err
+ }
+ extraTxOuts := auxShapeTxOuts(c.auxShape)
+
// Given the target fee-per-kw, we'll compute what our ideal _total_
// fee will be starting at for this fee negotiation.
c.idealFeeSat = c.cfg.FeeEstimator.EstimateFee(
- 0, localTxOut, remoteTxOut, c.idealFeeRate,
+ 0, localTxOut, remoteTxOut, extraTxOuts, c.idealFeeRate,
)
// When we're the initiator, we'll want to also factor in the highest
@@ -363,7 +454,7 @@ func (c *ChanCloser) initFeeBaseline() {
c.maxFee = c.idealFeeSat * defaultMaxFeeMultiplier
if c.cfg.MaxFee > 0 {
c.maxFee = c.cfg.FeeEstimator.EstimateFee(
- 0, localTxOut, remoteTxOut, c.cfg.MaxFee,
+ 0, localTxOut, remoteTxOut, extraTxOuts, c.cfg.MaxFee,
)
}
@@ -373,6 +464,8 @@ func (c *ChanCloser) initFeeBaseline() {
chancloserLog.Infof("Ideal fee for closure of ChannelPoint(%v) "+
"is: %v sat (max_fee=%v sat)", c.cfg.Channel.ChannelPoint(),
int64(c.idealFeeSat), int64(c.maxFee))
+
+ return nil
}
// initChanShutdown begins the shutdown process by un-registering the channel,
@@ -752,7 +845,10 @@ func (c *ChanCloser) BeginNegotiation() (fn.Option[lnwire.ClosingSigned],
case closeAwaitingFlush:
// Now that we know their desired delivery script, we can
// compute what our max/ideal fee will be.
- c.initFeeBaseline()
+ err := c.initFeeBaseline()
+ if err != nil {
+ return noClosingSigned, err
+ }
// At this point, we can now start the fee negotiation state, by
// constructing and sending our initial signature for what we
@@ -952,6 +1048,10 @@ func (c *ChanCloser) ReceiveClosingSigned( //nolint:funlen
if err != nil {
return noClosing, err
}
+ err = validateAuxShape(c.auxShape, c.auxOutputs)
+ if err != nil {
+ return noClosing, err
+ }
c.auxOutputs.WhenSome(func(outs AuxCloseOutputs) {
closeOpts = append(
closeOpts, lnwallet.WithExtraCloseOutputs(
@@ -1023,23 +1123,51 @@ func (c *ChanCloser) ReceiveClosingSigned( //nolint:funlen
}
}
+// auxShutdownReq assembles the shutdown request that describes this channel
+// to the aux closer.
+func (c *ChanCloser) auxShutdownReq() types.AuxShutdownReq {
+ return types.AuxShutdownReq{
+ ChanPoint: c.chanPoint,
+ ShortChanID: c.cfg.Channel.ShortChanID(),
+ InternalKey: c.localInternalKey,
+ Initiator: c.cfg.Channel.IsInitiator(),
+ CommitBlob: c.cfg.Channel.LocalCommitmentBlob(),
+ FundingBlob: c.cfg.Channel.FundingBlob(),
+ }
+}
+
+// auxCloseShape returns the fee-independent shape of any additional outputs
+// that will be added to the close transaction.
+func (c *ChanCloser) auxCloseShape() (fn.Option[AuxCloseShape], error) {
+ var closeShape fn.Option[AuxCloseShape]
+ err := fn.MapOptionZ(c.cfg.AuxCloser, func(aux AuxChanCloser) error {
+ shape, err := aux.AuxCloseShape(types.AuxCloseShapeDesc{
+ AuxShutdownReq: c.auxShutdownReq(),
+ })
+ if err != nil {
+ return err
+ }
+
+ closeShape = shape
+
+ return nil
+ })
+ if err != nil {
+ return closeShape, err
+ }
+
+ return closeShape, nil
+}
+
// auxCloseOutputs returns any additional outputs that should be used when
// closing the channel.
func (c *ChanCloser) auxCloseOutputs(
closeFee btcutil.Amount) (fn.Option[AuxCloseOutputs], error) {
var closeOuts fn.Option[AuxCloseOutputs]
err := fn.MapOptionZ(c.cfg.AuxCloser, func(aux AuxChanCloser) error {
- req := types.AuxShutdownReq{
- ChanPoint: c.chanPoint,
- ShortChanID: c.cfg.Channel.ShortChanID(),
- InternalKey: c.localInternalKey,
- Initiator: c.cfg.Channel.IsInitiator(),
- CommitBlob: c.cfg.Channel.LocalCommitmentBlob(),
- FundingBlob: c.cfg.Channel.FundingBlob(),
- }
outs, err := aux.AuxCloseOutputs(types.AuxCloseDesc{
- AuxShutdownReq: req,
+ AuxShutdownReq: c.auxShutdownReq(),
CloseFee: closeFee,
CommitFee: c.cfg.Channel.CommitFee(),
LocalCloseOutput: c.localCloseOutput,
@@ -1086,6 +1214,9 @@ func (c *ChanCloser) proposeCloseSigned(fee btcutil.Amount) (
if err != nil {
return nil, err
}
+ if err := validateAuxShape(c.auxShape, c.auxOutputs); err != nil {
+ return nil, err
+ }
c.auxOutputs.WhenSome(func(outs AuxCloseOutputs) {
closeOpts = append(
closeOpts, lnwallet.WithExtraCloseOutputs(
### lnwallet/chancloser/chancloser_test.go
@@ -21,6 +21,7 @@ import (
"github.com/lightningnetwork/lnd/lnutils"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
+ wallettypes "github.com/lightningnetwork/lnd/lnwallet/types"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/tlv"
"github.com/stretchr/testify/require"
@@ -311,12 +312,56 @@ func (m *mockMusigSession) ClosingNonce() (*musig2.Nonces, error) {
}, nil
}
+type mockAuxChanCloser struct {
+ extraScript []byte
+}
+
+func (m *mockAuxChanCloser) ShutdownBlob(
+ req wallettypes.AuxShutdownReq,
+) (fn.Option[lnwire.CustomRecords], error) {
+
+ return fn.None[lnwire.CustomRecords](), nil
+}
+
+func (m *mockAuxChanCloser) AuxCloseShape(
+ desc wallettypes.AuxCloseShapeDesc) (fn.Option[AuxCloseShape], error) {
+
+ return fn.Some(AuxCloseShape{
+ Outputs: []AuxCloseOutputShape{{
+ IsLocal: true,
+ PkScriptSize: len(m.extraScript),
+ }},
+ }), nil
+}
+
+func (m *mockAuxChanCloser) AuxCloseOutputs(
+ desc wallettypes.AuxCloseDesc) (fn.Option[AuxCloseOutputs], error) {
+
+ closeOutputs := []lnwallet.CloseOutput{{
+ TxOut: wire.TxOut{
+ PkScript: m.extraScript,
+ Value: 0,
+ },
+ IsLocal: true,
+ }}
+
+ return fn.Some(AuxCloseOutputs{
+ ExtraCloseOutputs: closeOutputs,
+ }), nil
+}
+
+func (m *mockAuxChanCloser) FinalizeClose(desc wallettypes.AuxCloseDesc,
+ closeTx *wire.MsgTx) error {
+
+ return nil
+}
+
type mockCoopFeeEstimator struct {
targetFee btcutil.Amount
}
func (m *mockCoopFeeEstimator) EstimateFee(chanType channeldb.ChannelType,
- localTxOut, remoteTxOut *wire.TxOut,
+ localTxOut, remoteTxOut *wire.TxOut, extraTxOuts []*wire.TxOut,
idealFeeRate chainfee.SatPerKWeight) btcutil.Amount {
return m.targetFee
@@ -377,13 +422,159 @@ func TestMaxFeeClamp(t *testing.T) {
// We'll call initFeeBaseline early here since we need
// the populate these internal variables.
- chanCloser.initFeeBaseline()
+ require.NoError(t, chanCloser.initFeeBaseline())
require.Equal(t, test.maxFee, chanCloser.maxFee)
})
}
}
+// TestInitFeeBaselineWithAuxCloseOutputs tests that aux close outputs are
+// accounted for in the initial fee baseline calculation.
+func TestInitFeeBaselineWithAuxCloseOutputs(t *testing.T) {
+ t.Parallel()
+
+ localScript := bytes.Repeat([]byte{0x11}, 34)
+ remoteScript := bytes.Repeat([]byte{0x22}, 34)
+ extraScript := bytes.Repeat([]byte{0x33}, 34)
+
+ channel := &mockChannel{
+ initiator: true,
+ }
+
+ newCloser := func(auxCloser fn.Option[AuxChanCloser]) *ChanCloser {
+ closer := NewChanCloser(
+ ChanCloseCfg{
+ Channel: channel,
+ FeeEstimator: &SimpleCoopFeeEstimator{},
+ AuxCloser: auxCloser,
+ },
+ DeliveryAddrWithKey{
+ DeliveryAddress: localScript,
+ },
+ chainfee.FeePerKwFloor, 0, nil, lntypes.Local,
+ )
+ closer.remoteDeliveryScript = remoteScript
+
+ return closer
+ }
+
+ closerNoAux := newCloser(fn.None[AuxChanCloser]())
+ require.NoError(t, closerNoAux.initFeeBaseline())
+
+ closerWithAux := newCloser(fn.Some[AuxChanCloser](&mockAuxChanCloser{
+ extraScript: extraScript,
+ }))
+ require.NoError(t, closerWithAux.initFeeBaseline())
+
+ localOutput := &wire.TxOut{
+ PkScript: localScript,
+ Value: 0,
+ }
+ remoteOutput := &wire.TxOut{
+ PkScript: remoteScript,
+ Value: 0,
+ }
+ extraOutput := &wire.TxOut{
+ PkScript: extraScript,
+ Value: 0,
+ }
+
+ expectedFeeNoAux := calcCoopCloseFee(
+ 0, localOutput, remoteOutput, nil, chainfee.FeePerKwFloor,
+ )
+ expectedFeeWithAux := calcCoopCloseFee(
+ 0, localOutput, remoteOutput, []*wire.TxOut{extraOutput},
+ chainfee.FeePerKwFloor,
+ )
+
+ require.Equal(t, expectedFeeNoAux, closerNoAux.idealFeeSat)
+ require.Equal(t, expectedFeeWithAux, closerWithAux.idealFeeSat)
+ require.Greater(t, closerWithAux.idealFeeSat, closerNoAux.idealFeeSat)
+}
+
+// TestValidateAuxShape tests that concrete aux close outputs are matched
+// against the shape declared for fee estimation, independent of ordering.
+func TestValidateAuxShape(t *testing.T) {
+ t.Parallel()
+
+ shape := func(outs ...AuxCloseOutputShape) fn.Option[AuxCloseShape] {
+ return fn.Some(AuxCloseShape{Outputs: outs})
+ }
+ outputs := func(outs ...lnwallet.CloseOutput) fn.Option[AuxCloseOutputs] { //nolint:ll
+ return fn.Some(AuxCloseOutputs{ExtraCloseOutputs: outs})
+ }
+ closeOut := func(isLocal bool, scriptSize int) lnwallet.CloseOutput {
+ return lnwallet.CloseOutput{
+ TxOut: wire.TxOut{
+ PkScript: make([]byte, scriptSize),
+ },
+ IsLocal: isLocal,
+ }
+ }
+
+ testCases := []struct {
+ name string
+ shape fn.Option[AuxCloseShape]
+ auxOutputs fn.Option[AuxCloseOutputs]
+ valid bool
+ }{{
+ name: "both absent",
+ shape: fn.None[AuxCloseShape](),
+ auxOutputs: fn.None[AuxCloseOutputs](),
+ valid: true,
+ }, {
+ name: "matching outputs in different order",
+ shape: shape(
+ AuxCloseOutputShape{IsLocal: true, PkScriptSize: 34},
+ AuxCloseOutputShape{IsLocal: false, PkScriptSize: 34},
+ ),
+ auxOutputs: outputs(
+ closeOut(false, 34), closeOut(true, 34),
+ ),
+ valid: true,
+ }, {
+ name: "undeclared output",
+ shape: fn.None[AuxCloseShape](),
+ auxOutputs: outputs(closeOut(true, 34)),
+ valid: false,
+ }, {
+ name: "missing output",
+ shape: shape(
+ AuxCloseOutputShape{IsLocal: true, PkScriptSize: 34},
+ ),
+ auxOutputs: fn.None[AuxCloseOutputs](),
+ valid: false,
+ }, {
+ name: "script size mismatch",
+ shape: shape(
+ AuxCloseOutputShape{IsLocal: true, PkScriptSize: 34},
+ ),
+ auxOutputs: outputs(closeOut(true, 35)),
+ valid: false,
+ }, {
+ name: "ownership mismatch",
+ shape: shape(
+ AuxCloseOutputShape{IsLocal: true, PkScriptSize: 34},
+ ),
+ auxOutputs: outputs(closeOut(false, 34)),
+ valid: false,
+ }}
+
+ for _, testCase := range testCases {
+ t.Run(testCase.name, func(t *testing.T) {
+ err := validateAuxShape(
+ testCase.shape, testCase.auxOutputs,
+ )
+ if testCase.valid {
+ require.NoError(t, err)
+ } else {
+ require.ErrorIs(t, err, ErrAuxShapeMismatch)
+ }
+ })
+ }
+}
+
// TestMaxFeeBailOut tests that once the negotiated fee rate rises above our
// maximum fee, we'll return an error and refuse to process a co-op close
// message.
@@ -541,7 +732,7 @@ func TestTaprootFastClose(t *testing.T) {
},
}, DeliveryAddrWithKey{}, idealFee, 0, nil, lntypes.Local,
)
- aliceCloser.initFeeBaseline()
+ require.NoError(t, aliceCloser.initFeeBaseline())
bobCloser := NewChanCloser(
ChanCloseCfg{
@@ -558,7 +749,7 @@ func TestTaprootFastClose(t *testing.T) {
},
}, DeliveryAddrWithKey{}, idealFee, 0, nil, lntypes.Remote,
)
- bobCloser.initFeeBaseline()
+ require.NoError(t, bobCloser.initFeeBaseline())
// With our set up complete, we'll now initialize the shutdown
// procedure kicked off by Alice.
### lnwallet/chancloser/interface.go
@@ -18,11 +18,13 @@ import (
type CoopFeeEstimator interface {
// EstimateFee estimates an _absolute_ fee for a co-op close transaction
// given the local+remote tx outs (for the co-op close transaction),
+ // any extra (e.g. auxiliary) outputs the close transaction will carry,
// channel type, and ideal fee rate. If a passed TxOut is nil, then
// that indicates that an output is dust on the co-op close transaction
// _before_ fees are accounted for.
EstimateFee(chanType channeldb.ChannelType,
localTxOut, remoteTxOut *wire.TxOut,
+ extraTxOuts []*wire.TxOut,
idealFeeRate chainfee.SatPerKWeight) btcutil.Amount
}
### lnwallet/chancloser/mock.go
@@ -89,10 +89,12 @@ type mockFeeEstimator struct {
}
func (m *mockFeeEstimator) EstimateFee(chanType channeldb.ChannelType,
- localTxOut, remoteTxOut *wire.TxOut,
+ localTxOut, remoteTxOut *wire.TxOut, extraTxOuts []*wire.TxOut,
idealFeeRate chainfee.SatPerKWeight) btcutil.Amount {
- args := m.Called(chanType, localTxOut, remoteTxOut, idealFeeRate)
+ args := m.Called(
+ chanType, localTxOut, remoteTxOut, extraTxOuts, idealFeeRate,
+ )
return args.Get(0).(btcutil.Amount)
}
### lnwallet/chancloser/rbf_coop_test.go
@@ -416,7 +416,7 @@ func (r *rbfCloserTestHarness) expectFeeEstimate(absoluteFee btcutil.Amount,
r.feeEstimator.On(
"EstimateFee", mock.Anything, mock.Anything, mock.Anything,
- mock.Anything,
+ mock.Anything, mock.Anything,
).Return(absoluteFee, nil).Times(numTimes)
}
### lnwallet/chancloser/rbf_coop_transitions.go
@@ -613,7 +613,7 @@ func (c *ChannelFlushing) ProcessEvent(event ProtocolEvent, env *Environment,
// we'd propose.
localTxOut, remoteTxOut := closeTerms.DeriveCloseTxOuts()
absoluteFee := env.FeeEstimator.EstimateFee(
- env.ChanType, localTxOut, remoteTxOut,
+ env.ChanType, localTxOut, remoteTxOut, nil,
idealFeeRate.FeePerKWeight(),
)
@@ -1135,7 +1135,7 @@ func (l *LocalCloseStart) ProcessEvent(event ProtocolEvent, env *Environment,
// First, we'll figure out the absolute fee rate we should pay
localTxOut, remoteTxOut := l.DeriveCloseTxOuts()
absoluteFee := env.FeeEstimator.EstimateFee(
- env.ChanType, localTxOut, remoteTxOut,
+ env.ChanType, localTxOut, remoteTxOut, nil,
msg.TargetFeeRate.FeePerKWeight(),
)
### lnwallet/types/close_types.go
@@ -51,6 +51,12 @@ type AuxShutdownReq struct {
FundingBlob fn.Option[tlv.Blob]
}
+// AuxCloseShapeDesc describes a channel close for which the fee-independent
+// shape of the auxiliary close outputs is being queried.
+type AuxCloseShapeDesc struct {
+ AuxShutdownReq
+}
+
// AuxCloseDesc is used to describe the channel close that is being performed.
type AuxCloseDesc struct {
AuxShutdownReq
### peer/musig_nonce_order_test.go
@@ -108,7 +108,7 @@ func TestRemoteCloseStartTaprootIntegration(t *testing.T) {
feeEstimator := &mockCoopFeeEstimator{}
feeEstimator.On(
"EstimateFee", mock.Anything, mock.Anything,
- mock.Anything, mock.Anything,
+ mock.Anything, mock.Anything, mock.Anything,
).Return(btcutil.Amount(1000))
chanObserver := &mockChanObserver{}
@@ -246,9 +246,12 @@ type mockCoopFeeEstimator struct {
func (m *mockCoopFeeEstimator) EstimateFee(
chanType channeldb.ChannelType, localTxOut, remoteTxOut *wire.TxOut,
+ extraTxOuts []*wire.TxOut,
idealFeeRate chainfee.SatPerKWeight) btcutil.Amount {
- args := m.Called(chanType, localTxOut, remoteTxOut, idealFeeRate)
+ args := m.Called(
+ chanType, localTxOut, remoteTxOut, extraTxOuts, idealFeeRate,
+ )
amt, _ := args.Get(0).(btcutil.Amount)
Why this scored 44/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.