-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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
chancloser+lnwire: add range-based negotiation #7062
Conversation
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
e6361bd
to
015d219
Compare
015d219
to
fa21e78
Compare
There was a problem hiding this 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.
lnwallet/chancloser/chancloser.go
Outdated
@@ -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 { |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's possible
There was a problem hiding this 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 { |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
Adding this comment so the bot sees it as active |
comment |
@Crypt-iQ, remember to re-request review from reviewers when ready |
Closing due to inactivity |
3 similar comments
Closing due to inactivity |
Closing due to inactivity |
Closing due to inactivity |
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:
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