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

lnwallet: Make available balance HTLC fee aware #3691

Merged
merged 9 commits into from
Feb 20, 2020
17 changes: 6 additions & 11 deletions htlcswitch/link.go
Original file line number Diff line number Diff line change
Expand Up @@ -2127,26 +2127,21 @@ func (l *channelLink) ChanID() lnwire.ChannelID {
//
// NOTE: Part of the ChannelLink interface.
func (l *channelLink) Bandwidth() lnwire.MilliSatoshi {
// Get the balance available on the channel for new HTLCs. This takes
halseth marked this conversation as resolved.
Show resolved Hide resolved
// the channel reserve into account so HTLCs up to this value won't
// violate it.
channelBandwidth := l.channel.AvailableBalance()
overflowBandwidth := l.overflowQueue.TotalHtlcAmount()

// To compute the total bandwidth, we'll take the current available
// bandwidth, then subtract the overflow bandwidth as we'll eventually
// also need to evaluate those HTLC's once space on the commitment
// transaction is free.
linkBandwidth := channelBandwidth - overflowBandwidth

// If the channel reserve is greater than the total available balance
// of the link, just return 0.
reserve := lnwire.NewMSatFromSatoshis(l.channel.LocalChanReserve())
if linkBandwidth < reserve {
overflowBandwidth := l.overflowQueue.TotalHtlcAmount()
if channelBandwidth < overflowBandwidth {
return 0
}

// Else the amount that is available to flow through the link at this
// point is the available balance minus the reserve amount we are
// required to keep as collateral.
return linkBandwidth - reserve
return channelBandwidth - overflowBandwidth
joostjager marked this conversation as resolved.
Show resolved Hide resolved
}

// AttachMailBox updates the current mailbox used by this link, and hooks up
Expand Down
23 changes: 15 additions & 8 deletions htlcswitch/link_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1982,8 +1982,11 @@ func TestChannelLinkBandwidthConsistency(t *testing.T) {
)

// The starting bandwidth of the channel should be exactly the amount
// that we created the channel between her and Bob.
expectedBandwidth := lnwire.NewMSatFromSatoshis(chanAmt - defaultCommitFee)
// that we created the channel between her and Bob, minus the
// commitment fee and fee for adding an additional HTLC.
expectedBandwidth := lnwire.NewMSatFromSatoshis(
chanAmt-defaultCommitFee,
) - htlcFee
assertLinkBandwidth(t, aliceLink, expectedBandwidth)

// Next, we'll create an HTLC worth 1 BTC, and send it into the link as
Expand Down Expand Up @@ -2656,8 +2659,10 @@ func TestChannelLinkTrimCircuitsPending(t *testing.T) {

// The starting bandwidth of the channel should be exactly the amount
// that we created the channel between her and Bob, minus the commitment
// fee.
expectedBandwidth := lnwire.NewMSatFromSatoshis(chanAmt - defaultCommitFee)
// fee and fee of adding an HTLC.
expectedBandwidth := lnwire.NewMSatFromSatoshis(
chanAmt-defaultCommitFee,
) - htlcFee
assertLinkBandwidth(t, alice.link, expectedBandwidth)

// Capture Alice's starting bandwidth to perform later, relative
Expand Down Expand Up @@ -2935,8 +2940,10 @@ func TestChannelLinkTrimCircuitsNoCommit(t *testing.T) {

// The starting bandwidth of the channel should be exactly the amount
// that we created the channel between her and Bob, minus the commitment
// fee.
expectedBandwidth := lnwire.NewMSatFromSatoshis(chanAmt - defaultCommitFee)
// fee and fee for adding an additional HTLC.
expectedBandwidth := lnwire.NewMSatFromSatoshis(
chanAmt-defaultCommitFee,
) - htlcFee
assertLinkBandwidth(t, alice.link, expectedBandwidth)

// Capture Alice's starting bandwidth to perform later, relative
Expand Down Expand Up @@ -3191,9 +3198,9 @@ func TestChannelLinkBandwidthChanReserve(t *testing.T) {

// The starting bandwidth of the channel should be exactly the amount
// that we created the channel between her and Bob, minus the channel
// reserve.
// reserve, commitment fee and fee for adding an additional HTLC.
expectedBandwidth := lnwire.NewMSatFromSatoshis(
chanAmt - defaultCommitFee - chanReserve)
chanAmt-defaultCommitFee-chanReserve) - htlcFee
assertLinkBandwidth(t, aliceLink, expectedBandwidth)

// Next, we'll create an HTLC worth 3 BTC, and send it into the link as
Expand Down
199 changes: 168 additions & 31 deletions lnwallet/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -3103,27 +3103,25 @@ func (lc *LightningChannel) getUnsignedAckedUpdates() []channeldb.LogUpdate {

// validateCommitmentSanity is used to validate the current state of the
// commitment transaction in terms of the ChannelConstraints that we and our
// remote peer agreed upon during the funding workflow. The predictAdded
// parameter should be set to a valid PaymentDescriptor if we are validating
// in the state when adding a new HTLC, or nil otherwise.
// remote peer agreed upon during the funding workflow. The
// predict[Our|Their]Add should parameters should be set to a valid
// PaymentDescriptor if we are validating in the state when adding a new HTLC,
// or nil otherwise.
func (lc *LightningChannel) validateCommitmentSanity(theirLogCounter,
ourLogCounter uint64, remoteChain bool,
predictAdded *PaymentDescriptor) error {
predictOurAdd, predictTheirAdd *PaymentDescriptor) error {

// Fetch all updates not committed.
view := lc.fetchHTLCView(theirLogCounter, ourLogCounter)

// If we are checking if we can add a new HTLC, we add this to the
// appropriate update log, in order to validate the sanity of the
// commitment resulting from _actually adding_ this HTLC to the state.
if predictAdded != nil {
// If the remoteChain bool is true, add to ourUpdates.
if remoteChain {
view.ourUpdates = append(view.ourUpdates, predictAdded)
} else {
// Else add to theirUpdates.
view.theirUpdates = append(view.theirUpdates, predictAdded)
}
if predictOurAdd != nil {
view.ourUpdates = append(view.ourUpdates, predictOurAdd)
}
if predictTheirAdd != nil {
view.theirUpdates = append(view.theirUpdates, predictTheirAdd)
}

commitChain := lc.localCommitChain
Expand Down Expand Up @@ -3296,7 +3294,7 @@ func (lc *LightningChannel) SignNextCommitment() (lnwire.Sig, []lnwire.Sig, []ch
// party set up when we initially set up the channel. If we are, then
// we'll abort this state transition.
err := lc.validateCommitmentSanity(
remoteACKedIndex, lc.localUpdateLog.logIndex, true, nil,
remoteACKedIndex, lc.localUpdateLog.logIndex, true, nil, nil,
)
if err != nil {
return sig, htlcSigs, nil, err
Expand Down Expand Up @@ -4050,7 +4048,7 @@ func (lc *LightningChannel) ReceiveNewCommitment(commitSig lnwire.Sig,
// the constraints we specified during initial channel setup. If not,
// then we'll abort the channel as they've violated our constraints.
err := lc.validateCommitmentSanity(
lc.remoteUpdateLog.logIndex, localACKedIndex, false, nil,
lc.remoteUpdateLog.logIndex, localACKedIndex, false, nil, nil,
)
if err != nil {
return err
Expand Down Expand Up @@ -4625,6 +4623,12 @@ func (lc *LightningChannel) InitNextRevocation(revKey *btcec.PublicKey) error {
// The additional openKey argument corresponds to the incoming CircuitKey of the
// committed circuit for this HTLC. This value should never be nil.
//
// Note that AddHTLC doesn't reserve the HTLC fee for future payment (like
// AvailableBalance does), so one could get into the "stuck channel" state by
// sending dust HTLCs.
// TODO(halseth): fix this either by using additional reserve, or better commit
// format. See https://github.com/lightningnetwork/lightning-rfc/issues/728
//
// NOTE: It is okay for sourceRef to be nil when unit testing the wallet.
func (lc *LightningChannel) AddHTLC(htlc *lnwire.UpdateAddHTLC,
openKey *channeldb.CircuitKey) (uint64, error) {
Expand All @@ -4644,10 +4648,26 @@ func (lc *LightningChannel) AddHTLC(htlc *lnwire.UpdateAddHTLC,
}

// Make sure adding this HTLC won't violate any of the constraints we
// must keep on our commitment transaction.
// must keep on the commitment transactions.
remoteACKedIndex := lc.localCommitChain.tail().theirMessageIndex

// First we'll check whether this HTLC can be added to the remote
// commitment transaction without violation any of the constraints.
err := lc.validateCommitmentSanity(
remoteACKedIndex, lc.localUpdateLog.logIndex, true, pd,
remoteACKedIndex, lc.localUpdateLog.logIndex, true, pd, nil,
)
if err != nil {
return 0, err
}

// We must also check whether it can be added to our own commitment
// transaction, or the remote node will refuse to sign. This is not
// totally bullet proof, as the remote might be adding updates
// concurrently, but if we fail this check there is for sure not
// possible for us to add the HTLC.
err = lc.validateCommitmentSanity(
Roasbeef marked this conversation as resolved.
Show resolved Hide resolved
lc.remoteUpdateLog.logIndex, lc.localUpdateLog.logIndex,
false, pd, nil,
)
if err != nil {
return 0, err
Expand Down Expand Up @@ -4685,7 +4705,7 @@ func (lc *LightningChannel) ReceiveHTLC(htlc *lnwire.UpdateAddHTLC) (uint64, err
// Clamp down on the number of HTLC's we can receive by checking the
// commitment sanity.
err := lc.validateCommitmentSanity(
lc.remoteUpdateLog.logIndex, localACKedIndex, false, pd,
lc.remoteUpdateLog.logIndex, localACKedIndex, false, nil, pd,
)
if err != nil {
return 0, err
Expand Down Expand Up @@ -5983,11 +6003,14 @@ func (lc *LightningChannel) CompleteCooperativeClose(localSig, remoteSig []byte,
return closeTx, ourBalance, nil
}

// AvailableBalance returns the current available balance within the channel.
// By available balance, we mean that if at this very instance s new commitment
// were to be created which evals all the log entries, what would our available
// balance me. This method is useful when deciding if a given channel can
// accept an HTLC in the multi-hop forwarding scenario.
// AvailableBalance returns the current balance available for sending within
// the channel. By available balance, we mean that if at this very instance a
// new commitment were to be created which evals all the log entries, what
// would our available balance for adding an additional HTLC be. It takes into
// account the fee that must be paid for adding this HTLC (if we're the
// initiator), and that we cannot spend from the channel reserve. This method
// is useful when deciding if a given channel can accept an HTLC in the
// multi-hop forwarding scenario.
func (lc *LightningChannel) AvailableBalance() lnwire.MilliSatoshi {
lc.RLock()
defer lc.RUnlock()
Expand All @@ -6007,19 +6030,132 @@ func (lc *LightningChannel) availableBalance() (lnwire.MilliSatoshi, int64) {
htlcView := lc.fetchHTLCView(remoteACKedIndex,
lc.localUpdateLog.logIndex)

// Then compute our current balance for that view.
ourBalance, _, commitWeight, filteredView, err :=
lc.computeView(htlcView, false, false)
// Calculate our available balance from our local commitment.
// TODO(halseth): could reuse parts validateCommitmentSanity to do this
// balance calculation, as most of the logic is the same.
//
// NOTE: This is not always accurate, since the remote node can always
// add updates concurrently, causing our balance to go down if we're
// the initiator, but this is a problem on the protocol level.
ourLocalCommitBalance, commitWeight := lc.availableCommitmentBalance(
htlcView, false,
)

// Do the same calculation from the remote commitment point of view.
ourRemoteCommitBalance, _ := lc.availableCommitmentBalance(
halseth marked this conversation as resolved.
Show resolved Hide resolved
htlcView, true,
)

// Return which ever balance is lowest.
if ourRemoteCommitBalance < ourLocalCommitBalance {
halseth marked this conversation as resolved.
Show resolved Hide resolved
return ourRemoteCommitBalance, commitWeight
}

return ourLocalCommitBalance, commitWeight
}

// availableCommitmentBalance attempts to calculate the balance we have
// available for HTLCs on the local/remote commitment given the htlcView. To
// account for sending HTLCs of different sizes, it will report the balance
// available for sending non-dust HTLCs, which will be manifested on the
// commitment, increasing the commitment fee we must pay as an initiator,
// eating into our balance. It will make sure we won't violate the channel
// reserve constraints for this amount.
func (lc *LightningChannel) availableCommitmentBalance(view *htlcView,
remoteChain bool) (lnwire.MilliSatoshi, int64) {
halseth marked this conversation as resolved.
Show resolved Hide resolved

// Compute the current balances for this commitment. This will take
// into account HTLCs to determine the commit weight, which the
// initiator must pay the fee for.
ourBalance, theirBalance, commitWeight, filteredView, err := lc.computeView(
view, remoteChain, false,
)
if err != nil {
lc.log.Errorf("Unable to fetch available balance: %v", err)
return 0, 0
}

// If we are the channel initiator, we must remember to subtract the
// commitment fee from our available balance.
commitFee := filteredView.feePerKw.FeeForWeight(commitWeight)
// We can never spend from the channel reserve, so we'll subtract it
// from our available balance.
ourReserve := lnwire.NewMSatFromSatoshis(
lc.channelState.LocalChanCfg.ChanReserve,
)
if ourReserve <= ourBalance {
ourBalance -= ourReserve
} else {
ourBalance = 0
}

// Calculate the commitment fee in the case where we would add another
// HTLC to the commitment, as only the balance remaining after this fee
// has been paid is actually available for sending.
feePerKw := filteredView.feePerKw
htlcCommitFee := lnwire.NewMSatFromSatoshis(
Copy link
Member

Choose a reason for hiding this comment

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

Similar to the predictAdded field in validateCommitmentSanity, shouldn't we only conditionally add this weight if we're attempting to look ahead by one HTLC to see the expected balance after this HTLC is added.

Copy link
Member

Choose a reason for hiding this comment

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

Or is the assumption that this method is only used atm by the switch when it is attempting to add a new HTLC to the commitment transaction?

Copy link
Member

Choose a reason for hiding this comment

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

Non blocking, more of a side point, but if we do add this param, then we can use this to display the current balance of a channel in ListChannels and WalletBalance, if we opt to just mask the concept of a channel reserver all together. If we go this route, then at least the user's wallet will show zero satoshis, rather than showing a balance which can't actually be sent/received (though there're other implications).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Or is the assumption that this method is only used atm by the switch when it is attempting to add a new HTLC to the commitment transaction?

Yeah, that is the assumption, that it returns the "balance available for HTLCs". And that has to include the HTLC fee if we are the initiator.

feePerKw.FeeForWeight(commitWeight + input.HTLCWeight),
)

// If we are the channel initiator, we must to subtract this commitment
// fee from our available balance in order to ensure we can afford both
// the value of the HTLC and the additional commitment fee from adding
// the HTLC.
if lc.channelState.IsInitiator {
ourBalance -= lnwire.NewMSatFromSatoshis(commitFee)
// There is an edge case where our non-zero balance is lower
// than the htlcCommitFee, where we could still be sending dust
// HTLCs, but we return 0 in this case. This is to avoid
// lowering our balance even further, as this takes us into a
// bad state wehere neither we nor our channel counterparty can
// add HTLCs.
if ourBalance < htlcCommitFee {
return 0, commitWeight
}

return ourBalance - htlcCommitFee, commitWeight
}

// If we're not the initiator, we must check whether the remote has
// enough balance to pay for the fee of our HTLC. We'll start by also
// subtracting our counterparty's reserve from their balance.
theirReserve := lnwire.NewMSatFromSatoshis(
lc.channelState.RemoteChanCfg.ChanReserve,
)
if theirReserve <= theirBalance {
theirBalance -= theirReserve
} else {
theirBalance = 0
}

// We'll use the dustlimit and htlcFee to find the largest HTLC value
// that will be considered dust on the commitment.
dustlimit := lnwire.NewMSatFromSatoshis(
halseth marked this conversation as resolved.
Show resolved Hide resolved
lc.channelState.LocalChanCfg.DustLimit,
)

// For an extra HTLC fee to be paid on our commitment, the HTLC must be
// large enough to make a non-dust HTLC timeout transaction.
htlcFee := lnwire.NewMSatFromSatoshis(
htlcTimeoutFee(feePerKw),
halseth marked this conversation as resolved.
Show resolved Hide resolved
)

// If we are looking at the remote commitment, we must use the remote
// dust limit and the fee for adding an HTLC success transaction.
if remoteChain {
dustlimit = lnwire.NewMSatFromSatoshis(
lc.channelState.RemoteChanCfg.DustLimit,
)
htlcFee = lnwire.NewMSatFromSatoshis(
htlcSuccessFee(feePerKw),
)
}

// The HTLC output will be manifested on the commitment if it
// is non-dust after paying the HTLC fee.
nonDustHtlcAmt := dustlimit + htlcFee

// If they cannot pay the fee if we add another non-dust HTLC, we'll
// report our available balance just below the non-dust amount, to
// avoid attempting HTLCs larger than this size.
if theirBalance < htlcCommitFee && ourBalance >= nonDustHtlcAmt {
ourBalance = nonDustHtlcAmt - 1
halseth marked this conversation as resolved.
Show resolved Hide resolved
}

return ourBalance, commitWeight
Expand Down Expand Up @@ -6230,13 +6366,14 @@ func (lc *LightningChannel) MaxFeeRate(maxAllocation float64) chainfee.SatPerKWe

// The maximum fee depends of the available balance that can be
// committed towards fees.
balance, weight := lc.availableBalance()
commit := lc.channelState.LocalCommitment
feeBalance := float64(
balance.ToSatoshis() + lc.channelState.LocalCommitment.CommitFee,
commit.LocalBalance.ToSatoshis() + commit.CommitFee,
)
maxFee := feeBalance * maxAllocation

// Ensure the fee rate doesn't dip below the fee floor.
_, weight := lc.availableBalance()
halseth marked this conversation as resolved.
Show resolved Hide resolved
maxFeeRate := maxFee / (float64(weight) / 1000)
return chainfee.SatPerKWeight(
math.Max(maxFeeRate, float64(chainfee.FeePerKwFloor)),
Expand Down
Loading