Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion docs/release-notes/release-notes-0.15.1.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Release Notes

## Protocol Extensions
## Protocol/Spec Updates

* [We'll now no longer clamp the co-op close fee to the commitment
fee](https://github.com/lightningnetwork/lnd/pull/6770). Instead, if users are
the initiator, they can now specify a max fee that should be respected.

### Zero-Conf Channel Opens
* [Introduces support for zero-conf channel opens and non-zero-conf option_scid_alias channels.](https://github.com/lightningnetwork/lnd/pull/5955)
Expand Down
9 changes: 8 additions & 1 deletion htlcswitch/switch.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,12 @@ type ChanClose struct {
// process for the cooperative closure transaction kicks off.
TargetFeePerKw chainfee.SatPerKWeight

// MaxFee is the highest fee the caller is willing to pay.
//
// NOTE: This field is only respected if the caller is the initiator of
// the channel.
MaxFee chainfee.SatPerKWeight

// DeliveryScript is an optional delivery script to pay funds out to.
DeliveryScript lnwire.DeliveryAddress

Expand Down Expand Up @@ -1656,7 +1662,7 @@ func (s *Switch) teardownCircuit(pkt *htlcPacket) error {
// optional parameter which sets a user specified script to close out to.
func (s *Switch) CloseLink(chanPoint *wire.OutPoint,
closeType contractcourt.ChannelCloseType,
targetFeePerKw chainfee.SatPerKWeight,
targetFeePerKw, maxFee chainfee.SatPerKWeight,
deliveryScript lnwire.DeliveryAddress) (chan interface{}, chan error) {

// TODO(roasbeef) abstract out the close updates.
Expand All @@ -1668,6 +1674,7 @@ func (s *Switch) CloseLink(chanPoint *wire.OutPoint,
ChanPoint: chanPoint,
Updates: updateChan,
TargetFeePerKw: targetFeePerKw,
MaxFee: maxFee,
DeliveryScript: deliveryScript,
Err: errChan,
}
Expand Down
3,990 changes: 2,002 additions & 1,988 deletions lnrpc/lightning.pb.go

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions lnrpc/lightning.proto
Original file line number Diff line number Diff line change
Expand Up @@ -1910,6 +1910,11 @@ message CloseChannelRequest {
// A manual fee rate set in sat/vbyte that should be used when crafting the
// closure transaction.
uint64 sat_per_vbyte = 6;

// The maximum fee rate the closer is willing to pay.
//
// NOTE: This field is only respected if we're the initiator of the channel.
uint64 max_fee_per_vbyte = 7;
}

message CloseStatusUpdate {
Expand Down
8 changes: 8 additions & 0 deletions lnrpc/lightning.swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -811,6 +811,14 @@
"required": false,
"type": "string",
"format": "uint64"
},
{
"name": "max_fee_per_vbyte",
"description": "The maximum fee rate the closer is willing to pay.\n\nNOTE: This field is only respected if we're the initiator of the channel.",
"in": "query",
"required": false,
"type": "string",
"format": "uint64"
}
],
"tags": [
Expand Down
113 changes: 90 additions & 23 deletions lnwallet/chancloser/chancloser.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import (

"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/davecgh/go-spew/spew"
"github.com/lightningnetwork/lnd/htlcswitch"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/labels"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
Expand All @@ -35,6 +37,12 @@ var (
// shutdown script previously set for that party.
ErrUpfrontShutdownScriptMismatch = fmt.Errorf("shutdown script does not " +
"match upfront shutdown script")

// ErrProposalExeceedsMaxFee is returned when as the initiator, the
// latest fee proposal sent by the responder exceed our max fee.
// responder.
ErrProposalExeceedsMaxFee = fmt.Errorf("latest fee proposal exceeds " +
"max fee")
)

// closeState represents all the possible states the channel closer state
Expand Down Expand Up @@ -73,11 +81,63 @@ const (
closeFinished
)

const (
// defaultMaxFeeMultiplier is a multiplier we'll apply to the ideal fee
// of the initiator, to decide when the negotiated fee is too high. By
// default, we want to bail out if we attempt to negotiate a fee that's
// 3x higher than our max fee.
defaultMaxFeeMultiplier = 3
)

// Channel abstracts away from the core channel state machine by exposing an
// interface that requires only the methods we need to carry out the channel
// closing process.
type Channel interface {
// CalcFee returns the absolute fee for the given fee rate.
CalcFee(chainfee.SatPerKWeight) btcutil.Amount

// ChannelPoint returns the channel point of the target channel.
ChannelPoint() *wire.OutPoint

// MarkCoopBroadcasted persistently marks that the channel close
// transaction has been broadcast.
MarkCoopBroadcasted(*wire.MsgTx, bool) error

// IsInitiator returns true we are the initiator of the channel.
IsInitiator() bool

// ShortChanID returns the scid of the channel.
ShortChanID() lnwire.ShortChannelID

// AbsoluteThawHeight returns the absolute thaw height of the channel.
// If the channel is pending, or an unconfirmed zero conf channel, then
// an error should be returned.
AbsoluteThawHeight() (uint32, error)

// RemoteUpfrontShutdownScript returns the upfront shutdown script of
// the remote party. If the remote party didn't specify such a script,
// an empty delivery address should be returned.
RemoteUpfrontShutdownScript() lnwire.DeliveryAddress

// CreateCloseProposal creates a new co-op close proposal in the form
// of a valid signature, the chainhash of the final txid, and our final
// balance in the created state.
CreateCloseProposal(proposedFee btcutil.Amount, localDeliveryScript []byte,
remoteDeliveryScript []byte) (input.Signature, *chainhash.Hash,
btcutil.Amount, error)

// CompleteCooperativeClose persistently "completes" the cooperative
// close by producing a fully signed co-op close transaction.
CompleteCooperativeClose(localSig, remoteSig input.Signature,
localDeliveryScript, remoteDeliveryScript []byte,
proposedFee btcutil.Amount) (*wire.MsgTx, btcutil.Amount, error)
}

// ChanCloseCfg holds all the items that a ChanCloser requires to carry out its
// duties.
type ChanCloseCfg struct {
// Channel is the channel that should be closed.
Channel *lnwallet.LightningChannel
Channel Channel

// BroadcastTx broadcasts the passed transaction to the network.
BroadcastTx func(*wire.MsgTx, string) error
Expand All @@ -89,6 +149,10 @@ type ChanCloseCfg struct {
// Disconnect will disconnect from the remote peer in this close.
Disconnect func() error

// MaxFee, is non-zero represents the highest fee that the initiator is
// willing to pay to close the channel.
MaxFee chainfee.SatPerKWeight

// Quit is a channel that should be sent upon in the occasion the state
// machine should cease all progress and shutdown.
Quit chan struct{}
Expand Down Expand Up @@ -122,6 +186,11 @@ type ChanCloser struct {
// offer when starting negotiation. This will be used as a baseline.
idealFeeSat btcutil.Amount

// maxFee is the highest fee the initiator is willing to pay to close
// out the channel. This is either a use specified value, or a default
// multiplier based of the initial starting ideal fee.
maxFee btcutil.Amount

// lastFeeProposal is the last fee that we proposed to the remote party.
// We'll use this as a pivot point to ratchet our next offer up, down, or
// simply accept the remote party's prior offer.
Expand Down Expand Up @@ -162,23 +231,16 @@ func NewChanCloser(cfg ChanCloseCfg, deliveryScript []byte,
idealFeePerKw chainfee.SatPerKWeight, negotiationHeight uint32,
closeReq *htlcswitch.ChanClose, locallyInitiated bool) *ChanCloser {

// Given the target fee-per-kw, we'll compute what our ideal _total_ fee
// will be starting at for this fee negotiation.
//
// TODO(roasbeef): should factor in minimal commit
// Given the target fee-per-kw, we'll compute what our ideal _total_
// fee will be starting at for this fee negotiation.
idealFeeSat := cfg.Channel.CalcFee(idealFeePerKw)

// If this fee is greater than the fee currently present within the
// commitment transaction, then we'll clamp it down to be within the proper
// range.
//
// TODO(roasbeef): clamp fee func?
channelCommitFee := cfg.Channel.StateSnapshot().CommitFee
if idealFeeSat > channelCommitFee {
chancloserLog.Infof("Ideal starting fee of %v is greater than commit "+
"fee of %v, clamping", int64(idealFeeSat), int64(channelCommitFee))

idealFeeSat = channelCommitFee
// When we're the initiator, we'll want to also factor in the highest
// fee we want to pay. This'll either be 3x the ideal fee, or the
// specified explicit max fee.
maxFee := idealFeeSat * defaultMaxFeeMultiplier
if cfg.MaxFee > 0 {
maxFee = cfg.Channel.CalcFee(cfg.MaxFee)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the ideal fee, the max fee isn't respected on restart

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I think one way to resolve this would be to commit this information to disk when we call MarkCoopBroadcasted.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we'd store persistently the preferences of the user at that point, and for the case where we're the responder (but still the initiator), we'd do something like base it off some (larger?) multiplier of the max commitment fee.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it should be stored in MarkCoopBroadcasted since #6760 will end up changing that function's usage. This is because the link may still need to be loaded, but we still want the user's preferences. I made a function called PersistDeliveryScript which could be extended to persist other information before calling MarkCoopBroadcasted

I also think we could check here that the ideal fee isn't greater than the max fee if one exists

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where we're the responder (but still the initiator), we'd do something like base it off some (larger?) multiplier of the max commitment fee.

and/or a config option?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So from my reading, we'll retain the "ideal" fee on restart, since we'll compute the "ideal" fee based on CoopCloseTargetConfs (defaults to 6 blocks). At this point, the req will be nil, so we'll then based the max fee off 3x this ideal fee.

So I think we can either leave it as is, persist it in #6760, or add a config option (yet another one, that ppl prob won't set tbh) to let ppl express what this max multiplier should be.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two ways the ideal fee is set:

  • through rpc if initiating the close
  • via EstimateFeePerKW on CoopCloseTargetConfs

The first case probably won't have the same fee on restart. The second may if the fee estimation doesn't change. I don't think this should be persisted in this PR. It could be done in #6760. But I think any persistence changes should be done in the same release so we don't have to check whether something is stored or not

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SGTM. I'd say a user's expectation would be for things to resume with their desired fee, so we should strive for that. Agree we can do it in another PR, though rn #6760 is marked for 0.16 (and in a draft state).

}

chancloserLog.Infof("Ideal fee for closure of ChannelPoint(%v) is: %v sat",
Expand All @@ -193,6 +255,7 @@ func NewChanCloser(cfg ChanCloseCfg, deliveryScript []byte,
cfg: cfg,
negotiationHeight: negotiationHeight,
idealFeeSat: idealFeeSat,
maxFee: maxFee,
localDeliveryScript: deliveryScript,
priorFeeOffers: make(map[btcutil.Amount]*lnwire.ClosingSigned),
locallyInitiated: locallyInitiated,
Expand Down Expand Up @@ -282,9 +345,13 @@ func (c *ChanCloser) CloseRequest() *htlcswitch.ChanClose {
return c.closeReq
}

// Channel returns the channel stored in the config.
// Channel returns the channel stored in the config as a
// *lnwallet.LightningChannel.
//
// NOTE: This method will PANIC if the underlying channel implementation isn't
// the desired type.
func (c *ChanCloser) Channel() *lnwallet.LightningChannel {
return c.cfg.Channel
return c.cfg.Channel.(*lnwallet.LightningChannel)
}

// NegotiationHeight returns the negotiation height.
Expand Down Expand Up @@ -352,9 +419,8 @@ func (c *ChanCloser) ProcessCloseMsg(msg lnwire.Message) ([]lnwire.Message,
// initiator of the channel opening, then we'll deny their close
// attempt.
chanInitiator := c.cfg.Channel.IsInitiator()
chanState := c.cfg.Channel.State()
if !chanInitiator {
absoluteThawHeight, err := chanState.AbsoluteThawHeight()
absoluteThawHeight, err := c.cfg.Channel.AbsoluteThawHeight()
if err != nil {
return nil, false, err
}
Expand Down Expand Up @@ -483,9 +549,10 @@ func (c *ChanCloser) ProcessCloseMsg(msg lnwire.Message) ([]lnwire.Message,
feeProposal := calcCompromiseFee(c.chanPoint, c.idealFeeSat,
c.lastFeeProposal, remoteProposedFee,
)
if feeProposal > c.idealFeeSat*3 {
return nil, false, fmt.Errorf("couldn't find" +
" compromise fee")
if c.cfg.Channel.IsInitiator() && feeProposal > c.maxFee {
return nil, false, fmt.Errorf("%w: %v > %v",
ErrProposalExeceedsMaxFee, feeProposal,
c.maxFee)
}

// With our new fee proposal calculated, we'll craft a new close
Expand Down
Loading