Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion channeldb/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -3446,7 +3446,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

db kvdb.Backend
}
Expand Down
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
63 changes: 63 additions & 0 deletions channeldb/models/inbound_fee.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package models

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

const (
feeRateParts = 1e6

// maxFeeRate is the maximum fee rate that we allow. It is set 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 _outgoing_ amount, 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. Always round down in favor of the
// payer of the fee. That way rounding differences can not cause a
// payment to fail.
switch {
case i.Rate > 0:
fee += rate * int64(amt) / feeRateParts

case i.Rate < 0:
fee += (rate*int64(amt) - (feeRateParts - 1)) /
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 down.
require.Equal(t, int64(-7), i.CalcFee(3))
}
28 changes: 25 additions & 3 deletions cmd/lncli/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -2075,6 +2075,12 @@ 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",
},
cli.Int64Flag{
Name: "inbound_fee_rate_ppm",
},
cli.Uint64Flag{
Name: "time_lock_delta",
Usage: "the CLTV delta that will be applied to all " +
Expand Down Expand Up @@ -2226,10 +2232,26 @@ func updateChannelPolicy(ctx *cli.Context) error {
}
}

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

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

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

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
12 changes: 12 additions & 0 deletions docs/release-notes/release-notes-0.18.0.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
# 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)
- [RPC Updates](#rpc-updates)
- [lncli Updates](#lncli-updates)
- [Code Health](#code-health)
Expand Down Expand Up @@ -35,6 +37,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 onchain
Expand Down Expand Up @@ -101,6 +112,7 @@
* Andras Banki-Horvath
* Carla Kirk-Cohen
* Elle Mouton
* Joost Jager
* Keagan McClelland
* Ononiwu Maureen Chiamaka
* Yong Yu
1 change: 1 addition & 0 deletions htlcswitch/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,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 @@ -2543,28 +2543,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 @@ -3111,6 +3126,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 @@ -3123,6 +3140,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 @@ -3175,6 +3193,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 @@ -3187,6 +3207,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