Merge pull request #6703 from bottlepay/inbound-fees

htlcswitch: add inbound routing fees receive support
This commit is contained in:
Olaoluwa Osuntokun 2024-03-31 08:43:09 -07:00 committed by GitHub
commit a6d4bb5c89
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 2156 additions and 1444 deletions

View file

@ -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

View file

@ -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

View file

@ -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
}

View file

@ -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))
}

View file

@ -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 " +
@ -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") {

View file

@ -1,4 +1,5 @@
# Release Notes
- [Release Notes](#release-notes)
- [Bug Fixes](#bug-fixes)
- [New Features](#new-features)
- [Functional Enhancements](#functional-enhancements)
@ -6,12 +7,14 @@
- [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)
@ -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
@ -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

View file

@ -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

View file

@ -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.
@ -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,
@ -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,
@ -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,
@ -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)

View file

@ -643,6 +643,206 @@ func testChannelLinkMultiHopPayment(t *testing.T,
}
}
func TestChannelLinkInboundFee(t *testing.T) {
t.Parallel()
t.Run("negative", func(t *testing.T) {
t.Parallel()
bobInboundFee := models.InboundFee{
Base: -500,
Rate: -100,
}
// Bob is supposed to sent Carol 1000000 msats. For this, he
// will charge an out fee of 1000 msat (the default hop network
// policy). Bob's inbound fee is based on the sum of outgoing
// htlc amount and the out fee that Bob charges. The value of
// this sum is 1001000. The proportional component of the
// inbound fee is -0.01% of the sum, which is -100 (rounded
// up). Added to this is the base inbound fee of -500, making
// for a total inbound fee of -600.
const expectedBobInFee = -600
testChannelLinkInboundFee(
t, bobInboundFee, expectedBobInFee, false,
)
})
t.Run("negative overpaid", func(t *testing.T) {
t.Parallel()
bobInboundFee := models.InboundFee{
Base: -500,
Rate: -100,
}
// Alice is not aware of the inbound discount and pays the full
// outbound fee.
const expectedBobInFee = 0
testChannelLinkInboundFee(
t, bobInboundFee, expectedBobInFee, false,
)
})
t.Run("negative total", func(t *testing.T) {
t.Parallel()
bobInboundFee := models.InboundFee{
Base: -5000,
}
const expectedBobInFee = -5000
// Bob's inbound discount exceeds his outbound fee. Forwards
// carrying a negative total fee should be rejected.
testChannelLinkInboundFee(
t, bobInboundFee, expectedBobInFee, true,
)
})
t.Run("positive", func(t *testing.T) {
t.Parallel()
bobInboundFee := models.InboundFee{
Base: 1_000,
Rate: 100_000,
}
const expectedBobInFee = 101_100
testChannelLinkInboundFee(
t, bobInboundFee, expectedBobInFee, false,
)
})
}
func testChannelLinkInboundFee(t *testing.T, //nolint:thelper
bobInboundFee models.InboundFee, expectedBobInFee int64,
expectedFail bool) {
channels, _, err := createClusterChannels(
t, btcutil.SatoshiPerBitcoin*3, btcutil.SatoshiPerBitcoin*5,
)
require.NoError(t, err, "unable to create channel")
n := newThreeHopNetwork(t, channels.aliceToBob, channels.bobToAlice,
channels.bobToCarol, channels.carolToBob, testStartingHeight)
require.NoError(t, n.start())
defer n.stop()
bobPolicy := n.globalPolicy
bobPolicy.InboundFee = bobInboundFee
n.firstBobChannelLink.UpdateForwardingPolicy(bobPolicy)
// Set an inbound fee for Carol. Because Carol is the payee, the fee
// should not be applied.
carolPolicy := n.globalPolicy
carolPolicy.InboundFee = models.InboundFee{
Base: -2_000,
Rate: -200_000,
}
n.carolChannelLink.UpdateForwardingPolicy(carolPolicy)
carolBandwidthBefore := n.carolChannelLink.Bandwidth()
firstBobBandwidthBefore := n.firstBobChannelLink.Bandwidth()
secondBobBandwidthBefore := n.secondBobChannelLink.Bandwidth()
aliceBandwidthBefore := n.aliceChannelLink.Bandwidth()
const (
expectedCarolInboundFee = 0
// Expect Bob's outbound fee to match the default hop network
// policy.
expectedBobOutboundFee = 1_000
)
amount := lnwire.MilliSatoshi(1_000_000)
htlcAmt := lnwire.MilliSatoshi(1_000_000 +
expectedCarolInboundFee + expectedBobOutboundFee +
expectedBobInFee,
)
totalTimelock := uint32(112)
hops := []*hop.Payload{
{
FwdInfo: hop.ForwardingInfo{
NextHop: n.carolChannelLink.
ShortChanID(),
AmountToForward: 1_000_000,
OutgoingCTLV: 106,
},
},
{
FwdInfo: hop.ForwardingInfo{
AmountToForward: 1_000_000,
OutgoingCTLV: 106,
},
},
}
receiver := n.carolServer
firstHop := n.firstBobChannelLink.ShortChanID()
rhash, err := makePayment(
n.aliceServer, n.carolServer, firstHop, hops, amount, htlcAmt,
totalTimelock,
).Wait(30 * time.Second)
if expectedFail {
require.Error(t, err)
return
}
require.NoError(t, err, "unable to send payment")
// Wait for Alice and Bob's second link to receive the revocation.
time.Sleep(2 * time.Second)
// Check that Carol invoice was settled and bandwidth of HTLC
// links were changed.
invoice, err := receiver.registry.LookupInvoice(
context.Background(), rhash,
)
require.NoError(t, err, "unable to get invoice")
require.Equal(t, invpkg.ContractSettled, invoice.State,
"carol invoice haven't been settled")
expectedAliceBandwidth := aliceBandwidthBefore - htlcAmt
require.Equalf(t,
expectedAliceBandwidth, n.aliceChannelLink.Bandwidth(),
"channel bandwidth incorrect: expected %v, got %v",
expectedAliceBandwidth, n.aliceChannelLink.Bandwidth(),
)
expectedBobBandwidth1 := firstBobBandwidthBefore + htlcAmt
require.Equalf(t,
expectedBobBandwidth1, n.firstBobChannelLink.Bandwidth(),
"channel bandwidth incorrect: expected %v, got %v",
expectedBobBandwidth1, n.firstBobChannelLink.Bandwidth(),
)
bobCarolDelta := lnwire.MilliSatoshi(
int64(amount) + expectedCarolInboundFee,
)
expectedBobBandwidth2 := secondBobBandwidthBefore - bobCarolDelta
require.Equalf(t,
expectedBobBandwidth2, n.secondBobChannelLink.Bandwidth(),
"channel bandwidth incorrect: expected %v, got %v",
expectedBobBandwidth2, n.secondBobChannelLink.Bandwidth(),
)
expectedCarolBandwidth := carolBandwidthBefore + bobCarolDelta
require.Equalf(t,
expectedCarolBandwidth, n.carolChannelLink.Bandwidth(),
"channel bandwidth incorrect: expected %v, got %v",
expectedCarolBandwidth, n.carolChannelLink.Bandwidth(),
)
}
// TestChannelLinkCancelFullCommitment tests the ability for links to cancel
// forwarded HTLCs once all of their commitment slots are full.
func TestChannelLinkCancelFullCommitment(t *testing.T) {
@ -5994,7 +6194,9 @@ func TestCheckHtlcForward(t *testing.T) {
t.Run("satisfied", func(t *testing.T) {
result := link.CheckHtlcForward(hash, 1500, 1000,
200, 150, 0, lnwire.ShortChannelID{})
200, 150, models.InboundFee{}, 0,
lnwire.ShortChannelID{},
)
if result != nil {
t.Fatalf("expected policy to be satisfied")
}
@ -6002,7 +6204,9 @@ func TestCheckHtlcForward(t *testing.T) {
t.Run("below minhtlc", func(t *testing.T) {
result := link.CheckHtlcForward(hash, 100, 50,
200, 150, 0, lnwire.ShortChannelID{})
200, 150, models.InboundFee{}, 0,
lnwire.ShortChannelID{},
)
if _, ok := result.WireMessage().(*lnwire.FailAmountBelowMinimum); !ok {
t.Fatalf("expected FailAmountBelowMinimum failure code")
}
@ -6010,7 +6214,9 @@ func TestCheckHtlcForward(t *testing.T) {
t.Run("above maxhtlc", func(t *testing.T) {
result := link.CheckHtlcForward(hash, 1500, 1200,
200, 150, 0, lnwire.ShortChannelID{})
200, 150, models.InboundFee{}, 0,
lnwire.ShortChannelID{},
)
if _, ok := result.WireMessage().(*lnwire.FailTemporaryChannelFailure); !ok {
t.Fatalf("expected FailTemporaryChannelFailure failure code")
}
@ -6018,7 +6224,9 @@ func TestCheckHtlcForward(t *testing.T) {
t.Run("insufficient fee", func(t *testing.T) {
result := link.CheckHtlcForward(hash, 1005, 1000,
200, 150, 0, lnwire.ShortChannelID{})
200, 150, models.InboundFee{}, 0,
lnwire.ShortChannelID{},
)
if _, ok := result.WireMessage().(*lnwire.FailFeeInsufficient); !ok {
t.Fatalf("expected FailFeeInsufficient failure code")
}
@ -6031,7 +6239,7 @@ func TestCheckHtlcForward(t *testing.T) {
result := link.CheckHtlcForward(
hash, 100005, 100000, 200,
150, 0, lnwire.ShortChannelID{},
150, models.InboundFee{}, 0, lnwire.ShortChannelID{},
)
_, ok := result.WireMessage().(*lnwire.FailFeeInsufficient)
require.True(t, ok, "expected FailFeeInsufficient failure code")
@ -6039,7 +6247,9 @@ func TestCheckHtlcForward(t *testing.T) {
t.Run("expiry too soon", func(t *testing.T) {
result := link.CheckHtlcForward(hash, 1500, 1000,
200, 150, 190, lnwire.ShortChannelID{})
200, 150, models.InboundFee{}, 190,
lnwire.ShortChannelID{},
)
if _, ok := result.WireMessage().(*lnwire.FailExpiryTooSoon); !ok {
t.Fatalf("expected FailExpiryTooSoon failure code")
}
@ -6047,7 +6257,9 @@ func TestCheckHtlcForward(t *testing.T) {
t.Run("incorrect cltv expiry", func(t *testing.T) {
result := link.CheckHtlcForward(hash, 1500, 1000,
200, 190, 0, lnwire.ShortChannelID{})
200, 190, models.InboundFee{}, 0,
lnwire.ShortChannelID{},
)
if _, ok := result.WireMessage().(*lnwire.FailIncorrectCltvExpiry); !ok {
t.Fatalf("expected FailIncorrectCltvExpiry failure code")
}
@ -6057,11 +6269,37 @@ func TestCheckHtlcForward(t *testing.T) {
t.Run("cltv expiry too far in the future", func(t *testing.T) {
// Check that expiry isn't too far in the future.
result := link.CheckHtlcForward(hash, 1500, 1000,
10200, 10100, 0, lnwire.ShortChannelID{})
10200, 10100, models.InboundFee{}, 0,
lnwire.ShortChannelID{},
)
if _, ok := result.WireMessage().(*lnwire.FailExpiryTooFar); !ok {
t.Fatalf("expected FailExpiryTooFar failure code")
}
})
t.Run("inbound fee satisfied", func(t *testing.T) {
t.Parallel()
result := link.CheckHtlcForward(hash, 1000+10-2-1, 1000,
200, 150, models.InboundFee{Base: -2, Rate: -1_000},
0, lnwire.ShortChannelID{})
if result != nil {
t.Fatalf("expected policy to be satisfied")
}
})
t.Run("inbound fee insufficient", func(t *testing.T) {
t.Parallel()
result := link.CheckHtlcForward(hash, 1000+10-10-101-1, 1000,
200, 150, models.InboundFee{Base: -10, Rate: -100_000},
0, lnwire.ShortChannelID{})
msg := result.WireMessage()
if _, ok := msg.(*lnwire.FailFeeInsufficient); !ok {
t.Fatalf("expected FailFeeInsufficient failure code")
}
})
}
// TestChannelLinkCanceledInvoice in this test checks the interaction

View file

@ -834,7 +834,7 @@ func (f *mockChannelLink) HandleChannelUpdate(lnwire.Message) {
func (f *mockChannelLink) UpdateForwardingPolicy(_ models.ForwardingPolicy) {
}
func (f *mockChannelLink) CheckHtlcForward([32]byte, lnwire.MilliSatoshi,
lnwire.MilliSatoshi, uint32, uint32, uint32,
lnwire.MilliSatoshi, uint32, uint32, models.InboundFee, uint32,
lnwire.ShortChannelID) *LinkError {
return f.checkHtlcForwardResult

View file

@ -2,6 +2,7 @@ package htlcswitch
import (
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/channeldb/models"
"github.com/lightningnetwork/lnd/htlcswitch/hop"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/record"
@ -103,6 +104,9 @@ type htlcPacket struct {
// but receives a channel_update with the alias SCID. Instead, the
// payer should receive a channel_update with the public SCID.
originalOutgoingChanID lnwire.ShortChannelID
// inboundFee is the fee schedule of the incoming channel.
inboundFee models.InboundFee
}
// inKey returns the circuit key used to identify the incoming htlc.

View file

@ -1178,7 +1178,9 @@ func (s *Switch) handlePacketForward(packet *htlcPacket) error {
failure = link.CheckHtlcForward(
htlc.PaymentHash, packet.incomingAmount,
packet.amount, packet.incomingTimeout,
packet.outgoingTimeout, currentHeight,
packet.outgoingTimeout,
packet.inboundFee,
currentHeight,
packet.originalOutgoingChanID,
)
}

View file

@ -74,14 +74,29 @@ func testMultiHopPayments(ht *lntest.HarnessTest) {
const aliceFeeRatePPM = 100000
updateChannelPolicy(
ht, alice, chanPointAlice, aliceBaseFeeSat*1000,
aliceFeeRatePPM, chainreg.DefaultBitcoinTimeLockDelta,
aliceFeeRatePPM, 0, 0, chainreg.DefaultBitcoinTimeLockDelta,
maxHtlc, carol,
)
// Define a negative inbound fee for Alice, to verify that this is
// backwards compatible with an older sender ignoring the discount.
const (
aliceInboundBaseFeeMsat = -1
aliceInboundFeeRate = -10000
)
updateChannelPolicy(
ht, alice, chanPointDave, 0, 0,
aliceInboundBaseFeeMsat, aliceInboundFeeRate,
chainreg.DefaultBitcoinTimeLockDelta, maxHtlc,
dave,
)
const daveBaseFeeSat = 5
const daveFeeRatePPM = 150000
updateChannelPolicy(
ht, dave, chanPointDave, daveBaseFeeSat*1000, daveFeeRatePPM,
0, 0,
chainreg.DefaultBitcoinTimeLockDelta, maxHtlc, carol,
)
@ -104,8 +119,9 @@ func testMultiHopPayments(ht *lntest.HarnessTest) {
ht.AssertAmountPaid("Alice(local) => Bob(remote)", alice,
chanPointAlice, expectedAmountPaidAtoB, int64(0))
// To forward a payment of 1000 sat, Alice is charging a fee of
// 1 sat + 10% = 101 sat.
// To forward a payment of 1000 sat, Alice is charging a fee of 1 sat +
// 10% = 101 sat. Note that this does not include the inbound fee
// (discount) because there is no sender support yet.
const aliceFeePerPayment = aliceBaseFeeSat +
(paymentAmt * aliceFeeRatePPM / 1_000_000)
const expectedFeeAlice = numPayments * aliceFeePerPayment
@ -224,15 +240,17 @@ func testMultiHopPayments(ht *lntest.HarnessTest) {
// NOTE: only used in current test.
func updateChannelPolicy(ht *lntest.HarnessTest, hn *node.HarnessNode,
chanPoint *lnrpc.ChannelPoint, baseFee int64,
feeRate int64, timeLockDelta uint32,
maxHtlc uint64, listenerNode *node.HarnessNode) {
feeRate int64, inboundBaseFee, inboundFeeRate int32,
timeLockDelta uint32, maxHtlc uint64, listenerNode *node.HarnessNode) {
expectedPolicy := &lnrpc.RoutingPolicy{
FeeBaseMsat: baseFee,
FeeRateMilliMsat: feeRate,
TimeLockDelta: timeLockDelta,
MinHtlc: 1000, // default value
MaxHtlcMsat: maxHtlc,
FeeBaseMsat: baseFee,
FeeRateMilliMsat: feeRate,
TimeLockDelta: timeLockDelta,
MinHtlc: 1000, // default value
MaxHtlcMsat: maxHtlc,
InboundFeeBaseMsat: inboundBaseFee,
InboundFeeRateMilliMsat: inboundFeeRate,
}
updateFeeReq := &lnrpc.PolicyUpdateRequest{
@ -242,7 +260,9 @@ func updateChannelPolicy(ht *lntest.HarnessTest, hn *node.HarnessNode,
Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{
ChanPoint: chanPoint,
},
MaxHtlcMsat: maxHtlc,
MaxHtlcMsat: maxHtlc,
InboundBaseFeeMsat: inboundBaseFee,
InboundFeeRatePpm: inboundFeeRate,
}
hn.RPC.UpdateChannelPolicy(updateFeeReq)

View file

@ -217,6 +217,14 @@
"format": "byte"
},
"description": "Custom channel update tlv records."
},
"inbound_fee_base_msat": {
"type": "integer",
"format": "int32"
},
"inbound_fee_rate_milli_msat": {
"type": "integer",
"format": "int32"
}
}
},

File diff suppressed because it is too large Load diff

View file

@ -3330,6 +3330,9 @@ message RoutingPolicy {
// Custom channel update tlv records.
map<uint64, bytes> custom_records = 8;
int32 inbound_fee_base_msat = 9;
int32 inbound_fee_rate_milli_msat = 10;
}
/*
@ -4309,7 +4312,15 @@ message ChannelFeeReport {
// The effective fee rate in milli-satoshis. Computed by dividing the
// fee_per_mil value by 1 million.
double fee_rate = 4;
// The base fee charged regardless of the number of milli-satoshis sent.
int32 inbound_base_fee_msat = 6;
// The amount charged per milli-satoshis transferred expressed in
// millionths of a satoshi.
int32 inbound_fee_per_mil = 7;
}
message FeeReportResponse {
// An array of channel fee reports which describes the current fee schedule
// for each channel.
@ -4360,7 +4371,11 @@ message PolicyUpdateRequest {
// If true, min_htlc_msat is applied.
bool min_htlc_msat_specified = 8;
int32 inbound_base_fee_msat = 10;
int32 inbound_fee_rate_ppm = 11;
}
enum UpdateFailure {
UPDATE_FAILURE_UNKNOWN = 0;
UPDATE_FAILURE_PENDING = 1;

View file

@ -4238,6 +4238,16 @@
"type": "number",
"format": "double",
"description": "The effective fee rate in milli-satoshis. Computed by dividing the\nfee_per_mil value by 1 million."
},
"inbound_base_fee_msat": {
"type": "integer",
"format": "int32",
"description": "The base fee charged regardless of the number of milli-satoshis sent."
},
"inbound_fee_per_mil": {
"type": "integer",
"format": "int32",
"description": "The amount charged per milli-satoshis transferred expressed in\nmillionths of a satoshi."
}
}
},
@ -6536,6 +6546,14 @@
"min_htlc_msat_specified": {
"type": "boolean",
"description": "If true, min_htlc_msat is applied."
},
"inbound_base_fee_msat": {
"type": "integer",
"format": "int32"
},
"inbound_fee_rate_ppm": {
"type": "integer",
"format": "int32"
}
}
},
@ -6840,6 +6858,14 @@
"format": "byte"
},
"description": "Custom channel update tlv records."
},
"inbound_fee_base_msat": {
"type": "integer",
"format": "int32"
},
"inbound_fee_rate_milli_msat": {
"type": "integer",
"format": "int32"
}
}
},

60
lnwire/typed_fee.go Normal file
View file

@ -0,0 +1,60 @@
package lnwire
import (
"io"
"github.com/lightningnetwork/lnd/tlv"
)
const (
FeeRecordType tlv.Type = 55555
)
// Fee represents a fee schedule.
type Fee struct {
BaseFee int32
FeeRate int32
}
// Record returns a TLV record that can be used to encode/decode the fee
// type from a given TLV stream.
func (l *Fee) Record() tlv.Record {
return tlv.MakeStaticRecord(
FeeRecordType, l, 8, feeEncoder, feeDecoder, //nolint:gomnd
)
}
// feeEncoder is a custom TLV encoder for the fee record.
func feeEncoder(w io.Writer, val interface{}, buf *[8]byte) error {
v, ok := val.(*Fee)
if !ok {
return tlv.NewTypeForEncodingErr(val, "lnwire.Fee")
}
if err := tlv.EUint32T(w, uint32(v.BaseFee), buf); err != nil {
return err
}
return tlv.EUint32T(w, uint32(v.FeeRate), buf)
}
// feeDecoder is a custom TLV decoder for the fee record.
func feeDecoder(r io.Reader, val interface{}, buf *[8]byte, l uint64) error {
v, ok := val.(*Fee)
if !ok {
return tlv.NewTypeForDecodingErr(val, "lnwire.Fee", l, 8)
}
var baseFee, feeRate uint32
if err := tlv.DUint32(r, &baseFee, buf, 4); err != nil { //nolint: gomnd,lll
return err
}
if err := tlv.DUint32(r, &feeRate, buf, 4); err != nil { //nolint: gomnd,lll
return err
}
v.FeeRate = int32(feeRate)
v.BaseFee = int32(baseFee)
return nil
}

40
lnwire/typed_fee_test.go Normal file
View file

@ -0,0 +1,40 @@
package lnwire
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestTypedFee(t *testing.T) {
t.Parallel()
t.Run("positive", func(t *testing.T) {
t.Parallel()
testTypedFee(t, Fee{
BaseFee: 10,
FeeRate: 20,
})
})
t.Run("negative", func(t *testing.T) {
t.Parallel()
testTypedFee(t, Fee{
BaseFee: -10,
FeeRate: -20,
})
})
}
func testTypedFee(t *testing.T, fee Fee) { //nolint: thelper
var eob ExtraOpaqueData
require.NoError(t, eob.PackRecords(&fee))
var extractedFee Fee
_, err := eob.ExtractRecords(&extractedFee)
require.NoError(t, err)
require.Equal(t, fee, extractedFee)
}

View file

@ -961,12 +961,26 @@ func (p *Brontide) loadActiveChannels(chans []*channeldb.OpenChannel) (
// routing policy into a forwarding policy.
var forwardingPolicy *models.ForwardingPolicy
if selfPolicy != nil {
var inboundWireFee lnwire.Fee
_, err := selfPolicy.ExtraOpaqueData.ExtractRecords(
&inboundWireFee,
)
if err != nil {
return nil, err
}
inboundFee := models.NewInboundFeeFromWire(
inboundWireFee,
)
forwardingPolicy = &models.ForwardingPolicy{
MinHTLCOut: selfPolicy.MinHTLC,
MaxHTLC: selfPolicy.MaxHTLC,
BaseFee: selfPolicy.FeeBaseMSat,
FeeRate: selfPolicy.FeeProportionalMillionths,
TimeLockDelta: uint32(selfPolicy.TimeLockDelta),
InboundFee: inboundFee,
}
} else {
p.log.Warnf("Unable to find our forwarding policy "+

View file

@ -112,6 +112,7 @@ func (r *Manager) UpdatePolicy(newSchema routing.ChannelPolicy,
TimeLockDelta: uint32(edge.TimeLockDelta),
MinHTLCOut: edge.MinHTLC,
MaxHTLC: edge.MaxHTLC,
InboundFee: newSchema.InboundFee,
}
return nil
@ -180,6 +181,12 @@ func (r *Manager) updateEdge(tx kvdb.RTx, chanPoint wire.OutPoint,
edge.FeeProportionalMillionths = lnwire.MilliSatoshi(
newSchema.FeeRate,
)
inboundFee := newSchema.InboundFee.ToWire()
if err := edge.ExtraOpaqueData.PackRecords(&inboundFee); err != nil {
return err
}
edge.TimeLockDelta = uint16(newSchema.TimeLockDelta)
// Retrieve negotiated channel htlc amt limits.

View file

@ -287,6 +287,10 @@ type FeeSchema struct {
// the effective fee rate charged per mSAT will be: (amount *
// FeeRate/1,000,000).
FeeRate uint32
// InboundFee is the inbound fee schedule that applies to forwards
// coming in through a channel to which this FeeSchema pertains.
InboundFee models.InboundFee
}
// ChannelPolicy holds the parameters that determine the policy we enforce
@ -2763,6 +2767,7 @@ func (r *ChannelRouter) applyChannelUpdate(msg *lnwire.ChannelUpdate) bool {
MaxHTLC: msg.HtlcMaximumMsat,
FeeBaseMSat: lnwire.MilliSatoshi(msg.BaseFee),
FeeProportionalMillionths: lnwire.MilliSatoshi(msg.FeeRate),
ExtraOpaqueData: msg.ExtraOpaqueData,
})
if err != nil && !IsError(err, ErrIgnored, ErrOutdated) {
log.Errorf("Unable to apply channel update: %v", err)

View file

@ -6059,6 +6059,23 @@ func marshalExtraOpaqueData(data []byte) map[uint64][]byte {
return records
}
// extractInboundFeeSafe tries to extract the inbound fee from the given extra
// opaque data tlv block. If parsing fails, a zero inbound fee is returned. This
// function is typically used on unvalidated data coming stored in the database.
// There is not much we can do other than ignoring errors here.
func extractInboundFeeSafe(data lnwire.ExtraOpaqueData) lnwire.Fee {
var inboundFee lnwire.Fee
_, err := data.ExtractRecords(&inboundFee)
if err != nil {
// Return zero fee. Do not return the inboundFee variable
// because it may be undefined.
return lnwire.Fee{}
}
return inboundFee
}
func marshalDBEdge(edgeInfo *models.ChannelEdgeInfo,
c1, c2 *models.ChannelEdgePolicy) *lnrpc.ChannelEdge {
@ -6108,6 +6125,7 @@ func marshalDBRoutingPolicy(
disabled := policy.ChannelFlags&lnwire.ChanUpdateDisabled != 0
customRecords := marshalExtraOpaqueData(policy.ExtraOpaqueData)
inboundFee := extractInboundFeeSafe(policy.ExtraOpaqueData)
return &lnrpc.RoutingPolicy{
TimeLockDelta: uint32(policy.TimeLockDelta),
@ -6118,6 +6136,9 @@ func marshalDBRoutingPolicy(
Disabled: disabled,
LastUpdate: uint32(policy.LastUpdate.Unix()),
CustomRecords: customRecords,
InboundFeeBaseMsat: inboundFee.BaseFee,
InboundFeeRateMilliMsat: inboundFee.FeeRate,
}
}
@ -6856,6 +6877,15 @@ func (r *rpcServer) FeeReport(ctx context.Context,
edgePolicy.FeeProportionalMillionths
feeRate := float64(feeRateFixedPoint) / feeBase
// Decode inbound fee from extra data.
var inboundFee lnwire.Fee
_, err := edgePolicy.ExtraOpaqueData.ExtractRecords(
&inboundFee,
)
if err != nil {
return err
}
// TODO(roasbeef): also add stats for revenue for each
// channel
feeReports = append(feeReports, &lnrpc.ChannelFeeReport{
@ -6864,6 +6894,9 @@ func (r *rpcServer) FeeReport(ctx context.Context,
BaseFeeMsat: int64(edgePolicy.FeeBaseMSat),
FeePerMil: int64(feeRateFixedPoint),
FeeRate: feeRate,
InboundBaseFeeMsat: inboundFee.BaseFee,
InboundFeePerMil: inboundFee.FeeRate,
})
return nil
@ -7050,6 +7083,10 @@ func (r *rpcServer) UpdateChannelPolicy(ctx context.Context,
feeSchema := routing.FeeSchema{
BaseFee: baseFeeMsat,
FeeRate: feeRateFixed,
InboundFee: models.InboundFee{
Base: req.InboundBaseFeeMsat,
Rate: req.InboundFeeRatePpm,
},
}
maxHtlc := lnwire.MilliSatoshi(req.MaxHtlcMsat)