Skip to content

Commit

Permalink
Merge pull request #6703 from bottlepay/inbound-fees
Browse files Browse the repository at this point in the history
htlcswitch: add inbound routing fees receive support
  • Loading branch information
Roasbeef authored Mar 31, 2024
2 parents 1d61de2 + ba21ca7 commit a6d4bb5
Show file tree
Hide file tree
Showing 23 changed files with 2,156 additions and 1,444 deletions.
3 changes: 3 additions & 0 deletions channeldb/models/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ type ForwardingPolicy struct {
// used to compute the required fee for a given HTLC.
FeeRate lnwire.MilliSatoshi

// InboundFee is the fee that must be paid for incoming HTLCs.
InboundFee InboundFee

// TimeLockDelta is the absolute time-lock value, expressed in blocks,
// that will be subtracted from an incoming HTLC's timelock value to
// create the time-lock value for the forwarded outgoing HTLC. The
Expand Down
2 changes: 1 addition & 1 deletion channeldb/models/channel_edge_policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ type ChannelEdgePolicy struct {
// properly validate the set of signatures that cover these new fields,
// and ensure we're able to make upgrades to the network in a forwards
// compatible manner.
ExtraOpaqueData []byte
ExtraOpaqueData lnwire.ExtraOpaqueData
}

// Signature is a channel announcement signature, which is needed for proper
Expand Down
53 changes: 53 additions & 0 deletions channeldb/models/inbound_fee.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package models

import "github.com/lightningnetwork/lnd/lnwire"

const (
// maxFeeRate is the maximum fee rate that we allow. It is set to allow
// a variable fee component of up to 10x the payment amount.
maxFeeRate = 10 * feeRateParts
)

type InboundFee struct {
Base int32
Rate int32
}

// NewInboundFeeFromWire constructs an inbound fee structure from a wire fee.
func NewInboundFeeFromWire(fee lnwire.Fee) InboundFee {
return InboundFee{
Base: fee.BaseFee,
Rate: fee.FeeRate,
}
}

// ToWire converts the inbound fee to a wire fee structure.
func (i *InboundFee) ToWire() lnwire.Fee {
return lnwire.Fee{
BaseFee: i.Base,
FeeRate: i.Rate,
}
}

// CalcFee calculates what the inbound fee should minimally be for forwarding
// the given amount. This amount is the total of the outgoing amount plus the
// outbound fee, which is what the inbound fee is based on.
func (i *InboundFee) CalcFee(amt lnwire.MilliSatoshi) int64 {
fee := int64(i.Base)
rate := int64(i.Rate)

// Cap the rate to prevent overflows.
switch {
case rate > maxFeeRate:
rate = maxFeeRate

case rate < -maxFeeRate:
rate = -maxFeeRate
}

// Calculate proportional component. To keep the integer math simple,
// positive fees are rounded down while negative fees are rounded up.
fee += rate * int64(amt) / feeRateParts

return fee
}
33 changes: 33 additions & 0 deletions channeldb/models/inbound_fee_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package models

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestInboundFee(t *testing.T) {
t.Parallel()

// Test positive fee.
i := InboundFee{
Base: 5,
Rate: 500000,
}

require.Equal(t, int64(6), i.CalcFee(2))

// Expect fee to be rounded down.
require.Equal(t, int64(6), i.CalcFee(3))

// Test negative fee.
i = InboundFee{
Base: -5,
Rate: -500000,
}

require.Equal(t, int64(-6), i.CalcFee(2))

// Expect fee to be rounded up.
require.Equal(t, int64(-6), i.CalcFee(3))
}
47 changes: 44 additions & 3 deletions cmd/lncli/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -2166,6 +2166,31 @@ var updateChannelPolicyCommand = cli.Command{
"0.000001 (millionths). Can not be set at " +
"the same time as fee_rate",
},
cli.Int64Flag{
Name: "inbound_base_fee_msat",
Usage: "the base inbound fee in milli-satoshis that " +
"will be charged for each forwarded HTLC, " +
"regardless of payment size. Its value must " +
"be zero or negative - it is a discount " +
"for using a particular incoming channel. " +
"Note that forwards will be rejected if the " +
"discount exceeds the outbound fee " +
"(forward at a loss), and lead to " +
"penalization by the sender",
},
cli.Int64Flag{
Name: "inbound_fee_rate_ppm",
Usage: "the inbound fee rate that will be charged " +
"proportionally based on the value of each " +
"forwarded HTLC and the outbound fee. Fee " +
"rate is expressed in parts per million and " +
"must be zero or negative - it is a discount " +
"for using a particular incoming channel." +
"Note that forwards will be rejected if the " +
"discount exceeds the outbound fee " +
"(forward at a loss), and lead to " +
"penalization by the sender",
},
cli.Uint64Flag{
Name: "time_lock_delta",
Usage: "the CLTV delta that will be applied to all " +
Expand Down Expand Up @@ -2318,10 +2343,26 @@ func updateChannelPolicy(ctx *cli.Context) error {
}
}

inboundBaseFeeMsat := ctx.Int64("inbound_base_fee_msat")
if inboundBaseFeeMsat < math.MinInt32 ||
inboundBaseFeeMsat > 0 {

return errors.New("inbound_base_fee_msat out of range")
}

inboundFeeRatePpm := ctx.Int64("inbound_fee_rate_ppm")
if inboundFeeRatePpm < math.MinInt32 ||
inboundFeeRatePpm > 0 {

return errors.New("inbound_fee_rate_ppm out of range")
}

req := &lnrpc.PolicyUpdateRequest{
BaseFeeMsat: baseFee,
TimeLockDelta: uint32(timeLockDelta),
MaxHtlcMsat: ctx.Uint64("max_htlc_msat"),
BaseFeeMsat: baseFee,
TimeLockDelta: uint32(timeLockDelta),
MaxHtlcMsat: ctx.Uint64("max_htlc_msat"),
InboundBaseFeeMsat: int32(inboundBaseFeeMsat),
InboundFeeRatePpm: int32(inboundFeeRatePpm),
}

if ctx.IsSet("min_htlc_msat") {
Expand Down
15 changes: 14 additions & 1 deletion docs/release-notes/release-notes-0.18.0.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
# Release Notes
- [Release Notes](#release-notes)
- [Bug Fixes](#bug-fixes)
- [New Features](#new-features)
- [Functional Enhancements](#functional-enhancements)
- [RPC Additions](#rpc-additions)
- [lncli Additions](#lncli-additions)
- [Improvements](#improvements)
- [Functional Updates](#functional-updates)
- [Tlv](#tlv)
- [Misc](#misc)
- [Logging](#logging)
- [RPC Updates](#rpc-updates)
- [lncli Updates](#lncli-updates)
- [Code Health](#code-health)
- [Breaking Changes](#breaking-changes)
- [Performance Improvements](#performance-improvements)
- [Misc](#misc)
- [Technical and Architectural Updates](#technical-and-architectural-updates)
- [BOLT Spec Updates](#bolt-spec-updates)
- [Testing](#testing)
Expand Down Expand Up @@ -109,6 +112,15 @@
# New Features
## Functional Enhancements

* Experimental support for [inbound routing
fees](https://github.com/lightningnetwork/lnd/pull/6703) is added. This allows
node operators to require senders to pay an inbound fee for forwards and
payments. It is recommended to only use negative fees (an inbound "discount")
initially to keep the channels open for senders that do not recognize inbound
fees. In this release, no send support for pathfinding and route building is
added yet. We first want to learn more about the impact that inbound fees have
on the routing economy.

* A new config value,
[sweeper.maxfeerate](https://github.com/lightningnetwork/lnd/pull/7823), is
added so users can specify the max allowed fee rate when sweeping on-chain
Expand Down Expand Up @@ -421,6 +433,7 @@ bitcoin peers' feefilter values into account](https://github.com/lightningnetwor
* Elle Mouton
* ErikEk
* Jesse de Wit
* Joost Jager
* Keagan McClelland
* Marcos Fernandez Perez
* Matt Morehouse
Expand Down
1 change: 1 addition & 0 deletions htlcswitch/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ type ChannelLink interface {
CheckHtlcForward(payHash [32]byte, incomingAmt lnwire.MilliSatoshi,
amtToForward lnwire.MilliSatoshi,
incomingTimeout, outgoingTimeout uint32,
inboundFee models.InboundFee,
heightNow uint32, scid lnwire.ShortChannelID) *LinkError

// CheckHtlcTransit should return a nil error if the passed HTLC details
Expand Down
39 changes: 30 additions & 9 deletions htlcswitch/link.go
Original file line number Diff line number Diff line change
Expand Up @@ -2780,28 +2780,43 @@ func (l *channelLink) UpdateForwardingPolicy(
func (l *channelLink) CheckHtlcForward(payHash [32]byte,
incomingHtlcAmt, amtToForward lnwire.MilliSatoshi,
incomingTimeout, outgoingTimeout uint32,
inboundFee models.InboundFee,
heightNow uint32, originalScid lnwire.ShortChannelID) *LinkError {

l.RLock()
policy := l.cfg.FwrdingPolicy
l.RUnlock()

// Using the amount of the incoming HTLC, we'll calculate the expected
// fee this incoming HTLC must carry in order to satisfy the
// constraints of the outgoing link.
expectedFee := ExpectedFee(policy, amtToForward)
// Using the outgoing HTLC amount, we'll calculate the outgoing
// fee this incoming HTLC must carry in order to satisfy the constraints
// of the outgoing link.
outFee := ExpectedFee(policy, amtToForward)

// Then calculate the inbound fee that we charge based on the sum of
// outgoing HTLC amount and outgoing fee.
inFee := inboundFee.CalcFee(amtToForward + outFee)

// Add up both fee components. It is important to calculate both fees
// separately. An alternative way of calculating is to first determine
// an aggregate fee and apply that to the outgoing HTLC amount. However,
// rounding may cause the result to be slightly higher than in the case
// of separately rounded fee components. This potentially causes failed
// forwards for senders and is something to be avoided.
expectedFee := inFee + int64(outFee)

// If the actual fee is less than our expected fee, then we'll reject
// this HTLC as it didn't provide a sufficient amount of fees, or the
// values have been tampered with, or the send used incorrect/dated
// information to construct the forwarding information for this hop. In
// any case, we'll cancel this HTLC. We're checking for this case first
// to leak as little information as possible.
actualFee := incomingHtlcAmt - amtToForward
// any case, we'll cancel this HTLC.
actualFee := int64(incomingHtlcAmt) - int64(amtToForward)
if incomingHtlcAmt < amtToForward || actualFee < expectedFee {
l.log.Warnf("outgoing htlc(%x) has insufficient fee: "+
"expected %v, got %v",
payHash[:], int64(expectedFee), int64(actualFee))
"expected %v, got %v: incoming=%v, outgoing=%v, "+
"inboundFee=%v",
payHash[:], expectedFee, actualFee,
incomingHtlcAmt, amtToForward, inboundFee,
)

// As part of the returned error, we'll send our latest routing
// policy so the sending node obtains the most up to date data.
Expand Down Expand Up @@ -3330,6 +3345,8 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg,
// round of processing.
chanIterator.EncodeNextHop(buf)

inboundFee := l.cfg.FwrdingPolicy.InboundFee

updatePacket := &htlcPacket{
incomingChanID: l.ShortChanID(),
incomingHTLCID: pd.HtlcIndex,
Expand All @@ -3342,6 +3359,7 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg,
incomingTimeout: pd.Timeout,
outgoingTimeout: fwdInfo.OutgoingCTLV,
customRecords: pld.CustomRecords(),
inboundFee: inboundFee,
}
switchPackets = append(
switchPackets, updatePacket,
Expand Down Expand Up @@ -3394,6 +3412,8 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg,
// have been added to switchPackets at the top of this
// section.
if fwdPkg.State == channeldb.FwdStateLockedIn {
inboundFee := l.cfg.FwrdingPolicy.InboundFee

updatePacket := &htlcPacket{
incomingChanID: l.ShortChanID(),
incomingHTLCID: pd.HtlcIndex,
Expand All @@ -3406,6 +3426,7 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg,
incomingTimeout: pd.Timeout,
outgoingTimeout: fwdInfo.OutgoingCTLV,
customRecords: pld.CustomRecords(),
inboundFee: inboundFee,
}

fwdPkg.FwdFilter.Set(idx)
Expand Down
Loading

0 comments on commit a6d4bb5

Please sign in to comment.