Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chancloser+lnwire: add range-based negotiation #7062

Closed

Conversation

Crypt-iQ
Copy link
Collaborator

@Crypt-iQ Crypt-iQ commented Oct 19, 2022

This PR adds FeeRange to ClosingSigned and makes lnd spec-compliant wrt FeeRange. Last 3 commits are new.

This patch also introduces the first place we'll send out a warning in lnd per the spec:

- if the message contains a fee_range:
  - if there is no overlap between that and its own fee_range:
    - SHOULD send a warning

Handling the warning is left for a future PR, since it is unclear what the funder should do as the FeeRange is already set and shouldn't change.

Fixes #4413

@Crypt-iQ Crypt-iQ added channel closing Related to the closing of channels cooperatively and uncooperatively fees Related to the fees paid for transactions (both LN and funding/commitment transactions) spec labels Oct 19, 2022
If upfront shutdown is not used, the delivery script would
previously not be persisted, leading to a spec violation on
retransmitting Shutdown.
This is accomplished by:
* passing Shutdown messages to the link when appropriate
* having the link tell peer.Brontide when closing_signed can be sent
* handling retransmission of Shutdown
* persisting the delivery scripts for retransmission
* reworking the ChanCloser such that closing_signed isn't immediately
sent after sending and receiving Shutdown
Copy link
Member

@Roasbeef Roasbeef left a comment

Choose a reason for hiding this comment

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

Thanks for picking this up! The initial set of changes looks pretty good, though I have some questions w.r.t if we can segregate some of the new logic a bit more cleanly.

One other recurring thread that pops into my mind is: users seem to want their co-op close to never fail. This is achievable in practice, if the responder simpler acquiesce to w/e the initiator wants, given that they pay the fees in the first place. The only edge case here is if the initiator's fees are so high to the point that your own output gets dusted.

@@ -375,6 +375,17 @@ func (c *ChanCloser) initChanShutdown() (*lnwire.Shutdown, error) {
c.chanPoint, err)
}

// Persist the delivery script used before marking the state as coop
// broadcasted so we can recover in case of a crash.
if len(c.Channel().LocalUpfrontShutdownScript()) == 0 {
Copy link
Member

Choose a reason for hiding this comment

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

How about we also commit the fee range at this point as well? This way we can deterministically resume if the connection dies for w/e reason.


// shutdownInit is a bool that is set when we've initiated a coop
// close.
shutdownInit bool
Copy link
Member

Choose a reason for hiding this comment

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

How about we encapsulate this new state into a smaller struct, something along the lines of shutdownContext or w/e? This way we can unit test it individually, and it may be a bit easier to review it in an isolated location vs new logic in the main event loop of the link.

)

// Flip the shutdownInit and shutdownSent bools.
l.shutdownInit = true
Copy link
Member

Choose a reason for hiding this comment

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

Doesn't need to be mutex protected?

@@ -1150,6 +1228,132 @@ func (l *channelLink) htlcManager() {
"PendingLocalUpdateCount")
}

// If we are attempting to shutdown the link to cooperatively
Copy link
Member

Choose a reason for hiding this comment

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

I think this should be extracted to a new method, also see above re further encapsulating all this logic in a new struct. This main loop is long enough already imo....

if err != nil {
l.fail(LinkFailureError{
code: ErrInternalError,
}, "failed persisting script: %v", err)
Copy link
Member

Choose a reason for hiding this comment

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

Should this report back to the caller (which now seemingly has more context on what happens here) that the attempt failed? Otherwise, iiuc, this'll just hang on RPC/CLI.


// GetOverlap takes two FeeRanges and returns the overlapping FeeRange between
// the two. If there is no overlap, nil is returned.
func (f *FeeRange) GetOverlap(other *FeeRange) *FeeRange {
Copy link
Member

Choose a reason for hiding this comment

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

👍

Copy link
Member

Choose a reason for hiding this comment

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

How about an error instead to make the error case more explicit?

// stream.
if val, ok := typeMap[FeeRangeType]; ok && val == nil {
// Check that the fee range is sane before setting it.
if fr.MinFeeSats > fr.MaxFeeSats {
Copy link
Member

Choose a reason for hiding this comment

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

Hmm, I think we might want to handle this check at a higher layer, particularly given that a concrete error isn't returned here, so the readHandler in the peer can't distinguish from violated constraints vs a garbled message (unparseable).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

IMO this check belongs here, it checks that the message is sane. We'd need a new function to check sanity otherwise, which accomplishes the same thing but requires another call in the chancloser

Copy link
Collaborator

Choose a reason for hiding this comment

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

why not have the check in the case closeFeeNegotiation: case in ProcessCloseMsg function where we also do the validation of other close messages? Tend to agree that this Decode message should only decode & not validate the meanings of the fields.

isFunder := c.cfg.Channel.IsInitiator()

// If FeeRange is set, perform FeeRange-specific checks.
if remoteMsg.FeeRange != nil {
Copy link
Member

Choose a reason for hiding this comment

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

I thought fee range was actually just in shutdown?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

it's in closing_signed

// between received and (about-to-be) sent
// fee_range.
if overlap == nil {
return nil, ErrNoRangeOverlap
Copy link
Member

Choose a reason for hiding this comment

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

What if instead we just have the funder send their desired fee rate? Based on inbound issues, people seem to want a trait where their co-op close actually never fails, instead deferring to w/e the funder wanted since they're the one paying for fees.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is a spec-level check, if the fundee does not send an overlapping fee-range, we fail

warning.ChanID = remoteMsg.ChannelID
warning.Data = lnwire.WarningData("ClosingSigned: no " +
"fee_range overlap")
return warning, nil
Copy link
Member

Choose a reason for hiding this comment

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

Similar here re the responder just accepting w/e the funder wants in the case of no overlap.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That's possible

Copy link
Collaborator

@ellemouton ellemouton left a comment

Choose a reason for hiding this comment

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

Left a few comments on the last 3 commits (since the commits before are covered by a different PR).
Main question is about why the funder just drops the ClosingSigned message that they construct

// stream.
if val, ok := typeMap[FeeRangeType]; ok && val == nil {
// Check that the fee range is sane before setting it.
if fr.MinFeeSats > fr.MaxFeeSats {
Copy link
Collaborator

Choose a reason for hiding this comment

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

why not have the check in the case closeFeeNegotiation: case in ProcessCloseMsg function where we also do the validation of other close messages? Tend to agree that this Decode message should only decode & not validate the meanings of the fields.

// - a message and no error (continue negotiation)
// - no message and an error (negotiation failed)
// - no message and no error (indicating we agree with the remote's fee)
func (c *ChanCloser) handleRemoteProposal(
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: might be worth splitting into handleRemoteProposalLegacy and handlRemoteProposalFeeRange or something just to avoid the huge indented code in an if-block

// Get the intersection of our two FeeRanges if one exists.
overlap := c.idealFeeRange.GetOverlap(remoteMsg.FeeRange)

if isFunder {
Copy link
Collaborator

Choose a reason for hiding this comment

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

excellent documentation in this method 💪

// - ...
// - otherwise:
// - MUST reply with the same fee_satoshis.
_, err := c.proposeCloseSigned(remoteMsg.FeeSatoshis)
Copy link
Collaborator

Choose a reason for hiding this comment

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

here we just construct a new ClosingSigned message but dont do anything with it? Should we not return the new message so that we send it to the peer?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

if we are agreeing proposeCloseSigned will add the fee to a map. then the calling function will check the map and send the closing signed. confusing but the logic is pre-existing

@ellemouton ellemouton closed this Jul 28, 2023
@Crypt-iQ Crypt-iQ reopened this Jul 28, 2023
@ellemouton
Copy link
Collaborator

Adding this comment so the bot sees it as active

@Crypt-iQ
Copy link
Collaborator Author

comment

@lightninglabs-deploy
Copy link

@Crypt-iQ, remember to re-request review from reviewers when ready

@lightninglabs-deploy
Copy link

Closing due to inactivity

3 similar comments
@lightninglabs-deploy
Copy link

Closing due to inactivity

@lightninglabs-deploy
Copy link

Closing due to inactivity

@lightninglabs-deploy
Copy link

Closing due to inactivity

@Crypt-iQ Crypt-iQ closed this Jan 24, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
channel closing Related to the closing of channels cooperatively and uncooperatively fees Related to the fees paid for transactions (both LN and funding/commitment transactions) spec
Projects
Status: High Priority
Development

Successfully merging this pull request may close these issues.

LND nodes overpay quite significantly on mutual closing TX's
4 participants