Skip to content

Commit a6d4bb5

Browse files
authored
Merge pull request #6703 from bottlepay/inbound-fees
htlcswitch: add inbound routing fees receive support
2 parents 1d61de2 + ba21ca7 commit a6d4bb5

23 files changed

+2156
-1444
lines changed

channeldb/models/channel.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@ type ForwardingPolicy struct {
115115
// used to compute the required fee for a given HTLC.
116116
FeeRate lnwire.MilliSatoshi
117117

118+
// InboundFee is the fee that must be paid for incoming HTLCs.
119+
InboundFee InboundFee
120+
118121
// TimeLockDelta is the absolute time-lock value, expressed in blocks,
119122
// that will be subtracted from an incoming HTLC's timelock value to
120123
// create the time-lock value for the forwarded outgoing HTLC. The

channeldb/models/channel_edge_policy.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ type ChannelEdgePolicy struct {
7070
// properly validate the set of signatures that cover these new fields,
7171
// and ensure we're able to make upgrades to the network in a forwards
7272
// compatible manner.
73-
ExtraOpaqueData []byte
73+
ExtraOpaqueData lnwire.ExtraOpaqueData
7474
}
7575

7676
// Signature is a channel announcement signature, which is needed for proper

channeldb/models/inbound_fee.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package models
2+
3+
import "github.com/lightningnetwork/lnd/lnwire"
4+
5+
const (
6+
// maxFeeRate is the maximum fee rate that we allow. It is set to allow
7+
// a variable fee component of up to 10x the payment amount.
8+
maxFeeRate = 10 * feeRateParts
9+
)
10+
11+
type InboundFee struct {
12+
Base int32
13+
Rate int32
14+
}
15+
16+
// NewInboundFeeFromWire constructs an inbound fee structure from a wire fee.
17+
func NewInboundFeeFromWire(fee lnwire.Fee) InboundFee {
18+
return InboundFee{
19+
Base: fee.BaseFee,
20+
Rate: fee.FeeRate,
21+
}
22+
}
23+
24+
// ToWire converts the inbound fee to a wire fee structure.
25+
func (i *InboundFee) ToWire() lnwire.Fee {
26+
return lnwire.Fee{
27+
BaseFee: i.Base,
28+
FeeRate: i.Rate,
29+
}
30+
}
31+
32+
// CalcFee calculates what the inbound fee should minimally be for forwarding
33+
// the given amount. This amount is the total of the outgoing amount plus the
34+
// outbound fee, which is what the inbound fee is based on.
35+
func (i *InboundFee) CalcFee(amt lnwire.MilliSatoshi) int64 {
36+
fee := int64(i.Base)
37+
rate := int64(i.Rate)
38+
39+
// Cap the rate to prevent overflows.
40+
switch {
41+
case rate > maxFeeRate:
42+
rate = maxFeeRate
43+
44+
case rate < -maxFeeRate:
45+
rate = -maxFeeRate
46+
}
47+
48+
// Calculate proportional component. To keep the integer math simple,
49+
// positive fees are rounded down while negative fees are rounded up.
50+
fee += rate * int64(amt) / feeRateParts
51+
52+
return fee
53+
}

channeldb/models/inbound_fee_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package models
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
)
8+
9+
func TestInboundFee(t *testing.T) {
10+
t.Parallel()
11+
12+
// Test positive fee.
13+
i := InboundFee{
14+
Base: 5,
15+
Rate: 500000,
16+
}
17+
18+
require.Equal(t, int64(6), i.CalcFee(2))
19+
20+
// Expect fee to be rounded down.
21+
require.Equal(t, int64(6), i.CalcFee(3))
22+
23+
// Test negative fee.
24+
i = InboundFee{
25+
Base: -5,
26+
Rate: -500000,
27+
}
28+
29+
require.Equal(t, int64(-6), i.CalcFee(2))
30+
31+
// Expect fee to be rounded up.
32+
require.Equal(t, int64(-6), i.CalcFee(3))
33+
}

cmd/lncli/commands.go

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2166,6 +2166,31 @@ var updateChannelPolicyCommand = cli.Command{
21662166
"0.000001 (millionths). Can not be set at " +
21672167
"the same time as fee_rate",
21682168
},
2169+
cli.Int64Flag{
2170+
Name: "inbound_base_fee_msat",
2171+
Usage: "the base inbound fee in milli-satoshis that " +
2172+
"will be charged for each forwarded HTLC, " +
2173+
"regardless of payment size. Its value must " +
2174+
"be zero or negative - it is a discount " +
2175+
"for using a particular incoming channel. " +
2176+
"Note that forwards will be rejected if the " +
2177+
"discount exceeds the outbound fee " +
2178+
"(forward at a loss), and lead to " +
2179+
"penalization by the sender",
2180+
},
2181+
cli.Int64Flag{
2182+
Name: "inbound_fee_rate_ppm",
2183+
Usage: "the inbound fee rate that will be charged " +
2184+
"proportionally based on the value of each " +
2185+
"forwarded HTLC and the outbound fee. Fee " +
2186+
"rate is expressed in parts per million and " +
2187+
"must be zero or negative - it is a discount " +
2188+
"for using a particular incoming channel." +
2189+
"Note that forwards will be rejected if the " +
2190+
"discount exceeds the outbound fee " +
2191+
"(forward at a loss), and lead to " +
2192+
"penalization by the sender",
2193+
},
21692194
cli.Uint64Flag{
21702195
Name: "time_lock_delta",
21712196
Usage: "the CLTV delta that will be applied to all " +
@@ -2318,10 +2343,26 @@ func updateChannelPolicy(ctx *cli.Context) error {
23182343
}
23192344
}
23202345

2346+
inboundBaseFeeMsat := ctx.Int64("inbound_base_fee_msat")
2347+
if inboundBaseFeeMsat < math.MinInt32 ||
2348+
inboundBaseFeeMsat > 0 {
2349+
2350+
return errors.New("inbound_base_fee_msat out of range")
2351+
}
2352+
2353+
inboundFeeRatePpm := ctx.Int64("inbound_fee_rate_ppm")
2354+
if inboundFeeRatePpm < math.MinInt32 ||
2355+
inboundFeeRatePpm > 0 {
2356+
2357+
return errors.New("inbound_fee_rate_ppm out of range")
2358+
}
2359+
23212360
req := &lnrpc.PolicyUpdateRequest{
2322-
BaseFeeMsat: baseFee,
2323-
TimeLockDelta: uint32(timeLockDelta),
2324-
MaxHtlcMsat: ctx.Uint64("max_htlc_msat"),
2361+
BaseFeeMsat: baseFee,
2362+
TimeLockDelta: uint32(timeLockDelta),
2363+
MaxHtlcMsat: ctx.Uint64("max_htlc_msat"),
2364+
InboundBaseFeeMsat: int32(inboundBaseFeeMsat),
2365+
InboundFeeRatePpm: int32(inboundFeeRatePpm),
23252366
}
23262367

23272368
if ctx.IsSet("min_htlc_msat") {

docs/release-notes/release-notes-0.18.0.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
# Release Notes
2+
- [Release Notes](#release-notes)
23
- [Bug Fixes](#bug-fixes)
34
- [New Features](#new-features)
45
- [Functional Enhancements](#functional-enhancements)
56
- [RPC Additions](#rpc-additions)
67
- [lncli Additions](#lncli-additions)
78
- [Improvements](#improvements)
89
- [Functional Updates](#functional-updates)
10+
- [Tlv](#tlv)
11+
- [Misc](#misc)
12+
- [Logging](#logging)
913
- [RPC Updates](#rpc-updates)
1014
- [lncli Updates](#lncli-updates)
1115
- [Code Health](#code-health)
1216
- [Breaking Changes](#breaking-changes)
1317
- [Performance Improvements](#performance-improvements)
14-
- [Misc](#misc)
1518
- [Technical and Architectural Updates](#technical-and-architectural-updates)
1619
- [BOLT Spec Updates](#bolt-spec-updates)
1720
- [Testing](#testing)
@@ -109,6 +112,15 @@
109112
# New Features
110113
## Functional Enhancements
111114

115+
* Experimental support for [inbound routing
116+
fees](https://github.com/lightningnetwork/lnd/pull/6703) is added. This allows
117+
node operators to require senders to pay an inbound fee for forwards and
118+
payments. It is recommended to only use negative fees (an inbound "discount")
119+
initially to keep the channels open for senders that do not recognize inbound
120+
fees. In this release, no send support for pathfinding and route building is
121+
added yet. We first want to learn more about the impact that inbound fees have
122+
on the routing economy.
123+
112124
* A new config value,
113125
[sweeper.maxfeerate](https://github.com/lightningnetwork/lnd/pull/7823), is
114126
added so users can specify the max allowed fee rate when sweeping on-chain
@@ -421,6 +433,7 @@ bitcoin peers' feefilter values into account](https://github.com/lightningnetwor
421433
* Elle Mouton
422434
* ErikEk
423435
* Jesse de Wit
436+
* Joost Jager
424437
* Keagan McClelland
425438
* Marcos Fernandez Perez
426439
* Matt Morehouse

htlcswitch/interfaces.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ type ChannelLink interface {
247247
CheckHtlcForward(payHash [32]byte, incomingAmt lnwire.MilliSatoshi,
248248
amtToForward lnwire.MilliSatoshi,
249249
incomingTimeout, outgoingTimeout uint32,
250+
inboundFee models.InboundFee,
250251
heightNow uint32, scid lnwire.ShortChannelID) *LinkError
251252

252253
// CheckHtlcTransit should return a nil error if the passed HTLC details

htlcswitch/link.go

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2780,28 +2780,43 @@ func (l *channelLink) UpdateForwardingPolicy(
27802780
func (l *channelLink) CheckHtlcForward(payHash [32]byte,
27812781
incomingHtlcAmt, amtToForward lnwire.MilliSatoshi,
27822782
incomingTimeout, outgoingTimeout uint32,
2783+
inboundFee models.InboundFee,
27832784
heightNow uint32, originalScid lnwire.ShortChannelID) *LinkError {
27842785

27852786
l.RLock()
27862787
policy := l.cfg.FwrdingPolicy
27872788
l.RUnlock()
27882789

2789-
// Using the amount of the incoming HTLC, we'll calculate the expected
2790-
// fee this incoming HTLC must carry in order to satisfy the
2791-
// constraints of the outgoing link.
2792-
expectedFee := ExpectedFee(policy, amtToForward)
2790+
// Using the outgoing HTLC amount, we'll calculate the outgoing
2791+
// fee this incoming HTLC must carry in order to satisfy the constraints
2792+
// of the outgoing link.
2793+
outFee := ExpectedFee(policy, amtToForward)
2794+
2795+
// Then calculate the inbound fee that we charge based on the sum of
2796+
// outgoing HTLC amount and outgoing fee.
2797+
inFee := inboundFee.CalcFee(amtToForward + outFee)
2798+
2799+
// Add up both fee components. It is important to calculate both fees
2800+
// separately. An alternative way of calculating is to first determine
2801+
// an aggregate fee and apply that to the outgoing HTLC amount. However,
2802+
// rounding may cause the result to be slightly higher than in the case
2803+
// of separately rounded fee components. This potentially causes failed
2804+
// forwards for senders and is something to be avoided.
2805+
expectedFee := inFee + int64(outFee)
27932806

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

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

3348+
inboundFee := l.cfg.FwrdingPolicy.InboundFee
3349+
33333350
updatePacket := &htlcPacket{
33343351
incomingChanID: l.ShortChanID(),
33353352
incomingHTLCID: pd.HtlcIndex,
@@ -3342,6 +3359,7 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg,
33423359
incomingTimeout: pd.Timeout,
33433360
outgoingTimeout: fwdInfo.OutgoingCTLV,
33443361
customRecords: pld.CustomRecords(),
3362+
inboundFee: inboundFee,
33453363
}
33463364
switchPackets = append(
33473365
switchPackets, updatePacket,
@@ -3394,6 +3412,8 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg,
33943412
// have been added to switchPackets at the top of this
33953413
// section.
33963414
if fwdPkg.State == channeldb.FwdStateLockedIn {
3415+
inboundFee := l.cfg.FwrdingPolicy.InboundFee
3416+
33973417
updatePacket := &htlcPacket{
33983418
incomingChanID: l.ShortChanID(),
33993419
incomingHTLCID: pd.HtlcIndex,
@@ -3406,6 +3426,7 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg,
34063426
incomingTimeout: pd.Timeout,
34073427
outgoingTimeout: fwdInfo.OutgoingCTLV,
34083428
customRecords: pld.CustomRecords(),
3429+
inboundFee: inboundFee,
34093430
}
34103431

34113432
fwdPkg.FwdFilter.Set(idx)

0 commit comments

Comments
 (0)