Skip to content

Commit 2dbda6c

Browse files
committed
htlcswitch: add receiver-side inbound fee support
1 parent 44db38d commit 2dbda6c

File tree

10 files changed

+386
-19
lines changed

10 files changed

+386
-19
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/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+
}

htlcswitch/interfaces.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ type ChannelLink interface {
203203
CheckHtlcForward(payHash [32]byte, incomingAmt lnwire.MilliSatoshi,
204204
amtToForward lnwire.MilliSatoshi,
205205
incomingTimeout, outgoingTimeout uint32,
206+
inboundFee models.InboundFee,
206207
heightNow uint32, scid lnwire.ShortChannelID) *LinkError
207208

208209
// 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
@@ -2546,28 +2546,43 @@ func (l *channelLink) UpdateForwardingPolicy(
25462546
func (l *channelLink) CheckHtlcForward(payHash [32]byte,
25472547
incomingHtlcAmt, amtToForward lnwire.MilliSatoshi,
25482548
incomingTimeout, outgoingTimeout uint32,
2549+
inboundFee models.InboundFee,
25492550
heightNow uint32, originalScid lnwire.ShortChannelID) *LinkError {
25502551

25512552
l.RLock()
25522553
policy := l.cfg.FwrdingPolicy
25532554
l.RUnlock()
25542555

2555-
// Using the amount of the incoming HTLC, we'll calculate the expected
2556-
// fee this incoming HTLC must carry in order to satisfy the
2557-
// constraints of the outgoing link.
2558-
expectedFee := ExpectedFee(policy, amtToForward)
2556+
// Using the outgoing HTLC amount, we'll calculate the outgoing
2557+
// fee this incoming HTLC must carry in order to satisfy the constraints
2558+
// of the outgoing link.
2559+
outFee := ExpectedFee(policy, amtToForward)
2560+
2561+
// Then calculate the inbound fee that we charge based on the sum of
2562+
// outgoing HTLC amount and outgoing fee.
2563+
inFee := inboundFee.CalcFee(amtToForward + outFee)
2564+
2565+
// Add up both fee components. It is important to calculate both fees
2566+
// separately. An alternative way of calculating is to first determine
2567+
// an aggregate fee and apply that to the outgoing HTLC amount. However,
2568+
// rounding may cause the result to be slightly higher than in the case
2569+
// of separately rounded fee components. This potentially causes failed
2570+
// forwards for senders and is something to be avoided.
2571+
expectedFee := inFee + int64(outFee)
25592572

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

25722587
// As part of the returned error, we'll send our latest routing
25732588
// policy so the sending node obtains the most up to date data.
@@ -3114,6 +3129,8 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg,
31143129
// round of processing.
31153130
chanIterator.EncodeNextHop(buf)
31163131

3132+
inboundFee := l.cfg.FwrdingPolicy.InboundFee
3133+
31173134
updatePacket := &htlcPacket{
31183135
incomingChanID: l.ShortChanID(),
31193136
incomingHTLCID: pd.HtlcIndex,
@@ -3126,6 +3143,7 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg,
31263143
incomingTimeout: pd.Timeout,
31273144
outgoingTimeout: fwdInfo.OutgoingCTLV,
31283145
customRecords: pld.CustomRecords(),
3146+
inboundFee: inboundFee,
31293147
}
31303148
switchPackets = append(
31313149
switchPackets, updatePacket,
@@ -3178,6 +3196,8 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg,
31783196
// have been added to switchPackets at the top of this
31793197
// section.
31803198
if fwdPkg.State == channeldb.FwdStateLockedIn {
3199+
inboundFee := l.cfg.FwrdingPolicy.InboundFee
3200+
31813201
updatePacket := &htlcPacket{
31823202
incomingChanID: l.ShortChanID(),
31833203
incomingHTLCID: pd.HtlcIndex,
@@ -3190,6 +3210,7 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg,
31903210
incomingTimeout: pd.Timeout,
31913211
outgoingTimeout: fwdInfo.OutgoingCTLV,
31923212
customRecords: pld.CustomRecords(),
3213+
inboundFee: inboundFee,
31933214
}
31943215

31953216
fwdPkg.FwdFilter.Set(idx)

0 commit comments

Comments
 (0)