Merge pull request #8886 from bitromortac/buildroute-inbound-fees

routing: inbound fees support for BuildRoute
This commit is contained in:
Oliver Gugger 2024-08-07 08:11:17 -06:00 committed by GitHub
commit b63e5decad
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 915 additions and 227 deletions

View file

@ -3688,7 +3688,7 @@ func TestLightningNodeSigVerification(t *testing.T) {
}
}
// TestComputeFee tests fee calculation based on both in- and outgoing amt.
// TestComputeFee tests fee calculation based on the outgoing amt.
func TestComputeFee(t *testing.T) {
var (
policy = models.ChannelEdgePolicy{
@ -3703,11 +3703,6 @@ func TestComputeFee(t *testing.T) {
if fee != expectedFee {
t.Fatalf("expected fee %v, got %v", expectedFee, fee)
}
fwdFee := policy.ComputeFeeFromIncoming(outgoingAmt + fee)
if fwdFee != expectedFee {
t.Fatalf("expected fee %v, but got %v", fee, fwdFee)
}
}
// TestBatchedAddChannelEdge asserts that BatchedAddChannelEdge properly

View file

@ -71,17 +71,6 @@ func (c *CachedEdgePolicy) ComputeFee(
return c.FeeBaseMSat + (amt*c.FeeProportionalMillionths)/feeRateParts
}
// ComputeFeeFromIncoming computes the fee to forward an HTLC given the incoming
// amount.
func (c *CachedEdgePolicy) ComputeFeeFromIncoming(
incomingAmt lnwire.MilliSatoshi) lnwire.MilliSatoshi {
return incomingAmt - divideCeil(
feeRateParts*(incomingAmt-c.FeeBaseMSat),
feeRateParts+c.FeeProportionalMillionths,
)
}
// NewCachedPolicy turns a full policy into a minimal one that can be cached.
func NewCachedPolicy(policy *ChannelEdgePolicy) *CachedEdgePolicy {
return &CachedEdgePolicy{

View file

@ -113,19 +113,3 @@ func (c *ChannelEdgePolicy) ComputeFee(
return c.FeeBaseMSat + (amt*c.FeeProportionalMillionths)/feeRateParts
}
// divideCeil divides dividend by factor and rounds the result up.
func divideCeil(dividend, factor lnwire.MilliSatoshi) lnwire.MilliSatoshi {
return (dividend + factor - 1) / factor
}
// ComputeFeeFromIncoming computes the fee to forward an HTLC given the incoming
// amount.
func (c *ChannelEdgePolicy) ComputeFeeFromIncoming(
incomingAmt lnwire.MilliSatoshi) lnwire.MilliSatoshi {
return incomingAmt - divideCeil(
feeRateParts*(incomingAmt-c.FeeBaseMSat),
feeRateParts+c.FeeProportionalMillionths,
)
}

View file

@ -102,6 +102,9 @@ commitment when the channel was force closed.
* [`ChanInfoRequest`](https://github.com/lightningnetwork/lnd/pull/8813)
adds support for channel points.
* [BuildRoute](https://github.com/lightningnetwork/lnd/pull/8886) now supports
inbound fees.
## lncli Updates
* [`importmc`](https://github.com/lightningnetwork/lnd/pull/8779) now accepts

View file

@ -15,6 +15,7 @@ import (
"github.com/btcsuite/btcd/wire"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
"github.com/lightningnetwork/lnd/lntypes"
@ -1400,6 +1401,10 @@ func (s *Server) trackPaymentStream(context context.Context,
func (s *Server) BuildRoute(_ context.Context,
req *BuildRouteRequest) (*BuildRouteResponse, error) {
if len(req.HopPubkeys) == 0 {
return nil, errors.New("no hops specified")
}
// Unmarshall hop list.
hops := make([]route.Vertex, len(req.HopPubkeys))
for i, pubkeyBytes := range req.HopPubkeys {
@ -1411,10 +1416,10 @@ func (s *Server) BuildRoute(_ context.Context,
}
// Prepare BuildRoute call parameters from rpc request.
var amt *lnwire.MilliSatoshi
var amt fn.Option[lnwire.MilliSatoshi]
if req.AmtMsat != 0 {
rpcAmt := lnwire.MilliSatoshi(req.AmtMsat)
amt = &rpcAmt
amt = fn.Some(rpcAmt)
}
var outgoingChan *uint64

View file

@ -5,6 +5,7 @@ import (
"context"
"fmt"
"math"
"math/big"
"sort"
"sync"
"sync/atomic"
@ -797,6 +798,7 @@ func generateSphinxPacket(rt *route.Route, paymentHash []byte,
hopCopy := sphinxPath[i]
path[i] = hopCopy
}
return spew.Sdump(path)
}),
)
@ -1350,24 +1352,22 @@ func (r *ChannelRouter) extractChannelUpdate(
// channels that satisfy all requirements.
type ErrNoChannel struct {
position int
fromNode route.Vertex
}
// Error returns a human-readable string describing the error.
func (e ErrNoChannel) Error() string {
return fmt.Sprintf("no matching outgoing channel available for "+
"node %v (%v)", e.position, e.fromNode)
"node index %v", e.position)
}
// BuildRoute returns a fully specified route based on a list of pubkeys. If
// amount is nil, the minimum routable amount is used. To force a specific
// outgoing channel, use the outgoingChan parameter.
func (r *ChannelRouter) BuildRoute(amt *lnwire.MilliSatoshi,
func (r *ChannelRouter) BuildRoute(amt fn.Option[lnwire.MilliSatoshi],
hops []route.Vertex, outgoingChan *uint64,
finalCltvDelta int32, payAddr *[32]byte) (*route.Route, error) {
log.Tracef("BuildRoute called: hopsCount=%v, amt=%v",
len(hops), amt)
log.Tracef("BuildRoute called: hopsCount=%v, amt=%v", len(hops), amt)
var outgoingChans map[uint64]struct{}
if outgoingChan != nil {
@ -1376,23 +1376,6 @@ func (r *ChannelRouter) BuildRoute(amt *lnwire.MilliSatoshi,
}
}
// If no amount is specified, we need to build a route for the minimum
// amount that this route can carry.
useMinAmt := amt == nil
var runningAmt lnwire.MilliSatoshi
if useMinAmt {
// For minimum amount routes, aim to deliver at least 1 msat to
// the destination. There are nodes in the wild that have a
// min_htlc channel policy of zero, which could lead to a zero
// amount payment being made.
runningAmt = 1
} else {
// If an amount is specified, we need to build a route that
// delivers exactly this amount to the final destination.
runningAmt = *amt
}
// We'll attempt to obtain a set of bandwidth hints that helps us select
// the best outgoing channel to use in case no outgoing channel is set.
bandwidthHints, err := newBandwidthManager(
@ -1402,6 +1385,49 @@ func (r *ChannelRouter) BuildRoute(amt *lnwire.MilliSatoshi,
return nil, err
}
sourceNode := r.cfg.SelfNode
// We check that each node in the route has a connection to others that
// can forward in principle.
unifiers, err := getEdgeUnifiers(
r.cfg.SelfNode, hops, outgoingChans, r.cfg.RoutingGraph,
)
if err != nil {
return nil, err
}
var (
receiverAmt lnwire.MilliSatoshi
senderAmt lnwire.MilliSatoshi
pathEdges []*unifiedEdge
)
// We determine the edges compatible with the requested amount, as well
// as the amount to send, which can be used to determine the final
// receiver amount, if a minimal amount was requested.
pathEdges, senderAmt, err = senderAmtBackwardPass(
unifiers, amt, bandwidthHints,
)
if err != nil {
return nil, err
}
// For the minimal amount search, we need to do a forward pass to find a
// larger receiver amount due to possible min HTLC bumps, otherwise we
// just use the requested amount.
receiverAmt, err = fn.ElimOption(
amt,
func() fn.Result[lnwire.MilliSatoshi] {
return fn.NewResult(
receiverAmtForwardPass(senderAmt, pathEdges),
)
},
fn.Ok[lnwire.MilliSatoshi],
).Unpack()
if err != nil {
return nil, err
}
// Fetch the current block height outside the routing transaction, to
// prevent the rpc call blocking the database.
_, height, err := r.cfg.Chain.GetBestBlock()
@ -1409,22 +1435,6 @@ func (r *ChannelRouter) BuildRoute(amt *lnwire.MilliSatoshi,
return nil, err
}
sourceNode := r.cfg.SelfNode
unifiers, senderAmt, err := getRouteUnifiers(
sourceNode, hops, useMinAmt, runningAmt, outgoingChans,
r.cfg.RoutingGraph, bandwidthHints,
)
if err != nil {
return nil, err
}
pathEdges, receiverAmt, err := getPathEdges(
sourceNode, senderAmt, unifiers, bandwidthHints, hops,
)
if err != nil {
return nil, err
}
// Build and return the final route.
return newRoute(
sourceNode, pathEdges, uint32(height),
@ -1438,12 +1448,10 @@ func (r *ChannelRouter) BuildRoute(amt *lnwire.MilliSatoshi,
)
}
// getRouteUnifiers returns a list of edge unifiers for the given route.
func getRouteUnifiers(source route.Vertex, hops []route.Vertex,
useMinAmt bool, runningAmt lnwire.MilliSatoshi,
outgoingChans map[uint64]struct{}, graph Graph,
bandwidthHints *bandwidthManager) ([]*edgeUnifier, lnwire.MilliSatoshi,
error) {
// getEdgeUnifiers returns a list of edge unifiers for the given route.
func getEdgeUnifiers(source route.Vertex, hops []route.Vertex,
outgoingChans map[uint64]struct{},
graph Graph) ([]*edgeUnifier, error) {
// Allocate a list that will contain the edge unifiers for this route.
unifiers := make([]*edgeUnifier, len(hops))
@ -1459,100 +1467,388 @@ func getRouteUnifiers(source route.Vertex, hops []route.Vertex,
fromNode = hops[i-1]
}
localChan := i == 0
// Build unified policies for this hop based on the channels
// known in the graph. Don't use inbound fees.
//
// TODO: Add inbound fees support for BuildRoute.
// known in the graph. Inbound fees are only active if the edge
// is not the last hop.
isExitHop := i == len(hops)-1
u := newNodeEdgeUnifier(
source, toNode, false, outgoingChans,
source, toNode, !isExitHop, outgoingChans,
)
err := u.addGraphPolicies(graph)
if err != nil {
return nil, 0, err
return nil, err
}
// Exit if there are no channels.
edgeUnifier, ok := u.edgeUnifiers[fromNode]
if !ok {
log.Errorf("Cannot find policy for node %v", fromNode)
return nil, 0, ErrNoChannel{
fromNode: fromNode,
position: i,
}
return nil, ErrNoChannel{position: i}
}
// If using min amt, increase amt if needed.
if useMinAmt {
min := edgeUnifier.minAmt()
if min > runningAmt {
runningAmt = min
}
}
// Get an edge for the specific amount that we want to forward.
edge := edgeUnifier.getEdge(runningAmt, bandwidthHints, 0)
if edge == nil {
log.Errorf("Cannot find policy with amt=%v for node %v",
runningAmt, fromNode)
return nil, 0, ErrNoChannel{
fromNode: fromNode,
position: i,
}
}
// Add fee for this hop.
if !localChan {
runningAmt += edge.policy.ComputeFee(runningAmt)
}
log.Tracef("Select channel %v at position %v",
edge.policy.ChannelID, i)
unifiers[i] = edgeUnifier
}
return unifiers, runningAmt, nil
return unifiers, nil
}
// getPathEdges returns the edges that make up the path and the total amount,
// including fees, to send the payment.
func getPathEdges(source route.Vertex, receiverAmt lnwire.MilliSatoshi,
unifiers []*edgeUnifier, bandwidthHints *bandwidthManager,
hops []route.Vertex) ([]*unifiedEdge,
lnwire.MilliSatoshi, error) {
// senderAmtBackwardPass returns a list of unified edges for the given route and
// determines the amount that should be sent to fulfill min HTLC requirements.
// The minimal sender amount can be searched for by using amt=None.
func senderAmtBackwardPass(unifiers []*edgeUnifier,
amt fn.Option[lnwire.MilliSatoshi],
bandwidthHints bandwidthHints) ([]*unifiedEdge, lnwire.MilliSatoshi,
error) {
if len(unifiers) == 0 {
return nil, 0, fmt.Errorf("no unifiers provided")
}
var unifiedEdges = make([]*unifiedEdge, len(unifiers))
// We traverse the route backwards and handle the last hop separately.
edgeUnifier := unifiers[len(unifiers)-1]
// incomingAmt tracks the amount that is forwarded on the edges of a
// route. The last hop only forwards the amount that the receiver should
// receive, as there are no fees paid to the last node.
// For minimum amount routes, aim to deliver at least 1 msat to
// the destination. There are nodes in the wild that have a
// min_htlc channel policy of zero, which could lead to a zero
// amount payment being made.
incomingAmt := amt.UnwrapOr(1)
// If using min amt, increase the amount if needed to fulfill min HTLC
// requirements.
if amt.IsNone() {
min := edgeUnifier.minAmt()
if min > incomingAmt {
incomingAmt = min
}
}
// Get an edge for the specific amount that we want to forward.
edge := edgeUnifier.getEdge(incomingAmt, bandwidthHints, 0)
if edge == nil {
log.Errorf("Cannot find policy with amt=%v "+
"for hop %v", incomingAmt, len(unifiers)-1)
return nil, 0, ErrNoChannel{position: len(unifiers) - 1}
}
unifiedEdges[len(unifiers)-1] = edge
// Handle the rest of the route except the last hop.
for i := len(unifiers) - 2; i >= 0; i-- {
edgeUnifier = unifiers[i]
// If using min amt, increase the amount if needed to fulfill
// min HTLC requirements.
if amt.IsNone() {
min := edgeUnifier.minAmt()
if min > incomingAmt {
incomingAmt = min
}
}
// A --current hop-- B --next hop: incomingAmt-- C
// The outbound fee paid to the current end node B is based on
// the amount that the next hop forwards and B's policy for that
// hop.
outboundFee := unifiedEdges[i+1].policy.ComputeFee(
incomingAmt,
)
netAmount := incomingAmt + outboundFee
// We need to select an edge that can forward the requested
// amount.
edge = edgeUnifier.getEdge(
netAmount, bandwidthHints, outboundFee,
)
if edge == nil {
return nil, 0, ErrNoChannel{position: i}
}
// The fee paid to B depends on the current hop's inbound fee
// policy and on the outbound fee for the next hop as any
// inbound fee discount is capped by the outbound fee such that
// the total fee for B can't become negative.
inboundFee := calcCappedInboundFee(edge, netAmount, outboundFee)
fee := lnwire.MilliSatoshi(int64(outboundFee) + inboundFee)
log.Tracef("Select channel %v at position %v",
edge.policy.ChannelID, i)
// Finally, we update the amount that needs to flow into node B
// from A, which is the next hop's forwarding amount plus the
// fee for B: A --current hop: incomingAmt-- B --next hop-- C
incomingAmt += fee
unifiedEdges[i] = edge
}
return unifiedEdges, incomingAmt, nil
}
// receiverAmtForwardPass returns the amount that a receiver will receive after
// deducting all fees from the sender amount.
func receiverAmtForwardPass(runningAmt lnwire.MilliSatoshi,
unifiedEdges []*unifiedEdge) (lnwire.MilliSatoshi, error) {
if len(unifiedEdges) == 0 {
return 0, fmt.Errorf("no edges to forward through")
}
inEdge := unifiedEdges[0]
if !inEdge.amtInRange(runningAmt) {
log.Errorf("Amount %v not in range for hop index %v",
runningAmt, 0)
return 0, ErrNoChannel{position: 0}
}
// Now that we arrived at the start of the route and found out the route
// total amount, we make a forward pass. Because the amount may have
// been increased in the backward pass, fees need to be recalculated and
// amount ranges re-checked.
var pathEdges []*unifiedEdge
for i, unifier := range unifiers {
edge := unifier.getEdge(receiverAmt, bandwidthHints, 0)
if edge == nil {
fromNode := source
if i > 0 {
fromNode = hops[i-1]
}
for i := 1; i < len(unifiedEdges); i++ {
inEdge := unifiedEdges[i-1]
outEdge := unifiedEdges[i]
return nil, 0, ErrNoChannel{
fromNode: fromNode,
position: i,
}
// Decrease the amount to send while going forward.
runningAmt = outgoingFromIncoming(runningAmt, inEdge, outEdge)
if !outEdge.amtInRange(runningAmt) {
log.Errorf("Amount %v not in range for hop index %v",
runningAmt, i)
return 0, ErrNoChannel{position: i}
}
if i > 0 {
// Decrease the amount to send while going forward.
receiverAmt -= edge.policy.ComputeFeeFromIncoming(
receiverAmt,
)
}
pathEdges = append(pathEdges, edge)
}
return pathEdges, receiverAmt, nil
return runningAmt, nil
}
// incomingFromOutgoing computes the incoming amount based on the outgoing
// amount by adding fees to the outgoing amount, replicating the path finding
// and routing process, see also CheckHtlcForward.
func incomingFromOutgoing(outgoingAmt lnwire.MilliSatoshi,
incoming, outgoing *unifiedEdge) lnwire.MilliSatoshi {
outgoingFee := outgoing.policy.ComputeFee(outgoingAmt)
// Net amount is the amount the inbound fees are calculated with.
netAmount := outgoingAmt + outgoingFee
inboundFee := incoming.inboundFees.CalcFee(netAmount)
// The inbound fee is not allowed to reduce the incoming amount below
// the outgoing amount.
if int64(outgoingFee)+inboundFee < 0 {
return outgoingAmt
}
return netAmount + lnwire.MilliSatoshi(inboundFee)
}
// outgoingFromIncoming computes the outgoing amount based on the incoming
// amount by subtracting fees from the incoming amount. Note that this is not
// exactly the inverse of incomingFromOutgoing, because of some rounding.
func outgoingFromIncoming(incomingAmt lnwire.MilliSatoshi,
incoming, outgoing *unifiedEdge) lnwire.MilliSatoshi {
// Convert all quantities to big.Int to be able to hande negative
// values. The formulas to compute the outgoing amount involve terms
// with PPM*PPM*A, which can easily overflow an int64.
A := big.NewInt(int64(incomingAmt))
Ro := big.NewInt(int64(outgoing.policy.FeeProportionalMillionths))
Bo := big.NewInt(int64(outgoing.policy.FeeBaseMSat))
Ri := big.NewInt(int64(incoming.inboundFees.Rate))
Bi := big.NewInt(int64(incoming.inboundFees.Base))
PPM := big.NewInt(1_000_000)
// The following discussion was contributed by user feelancer21, see
//nolint:lll
// https://github.com/feelancer21/lnd/commit/f6f05fa930985aac0d27c3f6681aada1b599162a.
// The incoming amount Ai based on the outgoing amount Ao is computed by
// Ai = max(Ai(Ao), Ao), which caps the incoming amount such that the
// total node fee (Ai - Ao) is non-negative. This is commonly enforced
// by routing nodes.
// The function Ai(Ao) is given by:
// Ai(Ao) = (Ao + Bo + Ro/PPM) + (Bi + (Ao + Ro/PPM + Bo)*Ri/PPM), where
// the first term is the net amount (the outgoing amount plus the
// outbound fee), and the second is the inbound fee computed based on
// the net amount.
// Ai(Ao) can potentially become more negative in absolute value than
// Ao, which is why the above mentioned capping is needed. We can
// abbreviate Ai(Ao) with Ai(Ao) = m*Ao + n, where m and n are:
// m := (1 + Ro/PPM) * (1 + Ri/PPM)
// n := Bi + Bo*(1 + Ri/PPM)
// If we know that m > 0, this is equivalent of Ri/PPM > -1, because Ri
// is the only factor that can become negative. A value or Ri/PPM = -1,
// means that the routing node is willing to give up on 100% of the
// net amount (based on the fee rate), which is likely to not happen in
// practice. This condition will be important for a later trick.
// If we want to compute the incoming amount based on the outgoing
// amount, which is the reverse problem, we need to solve Ai =
// max(Ai(Ao), Ao) for Ao(Ai). Given an incoming amount A,
// we look for an Ao such that A = max(Ai(Ao), Ao).
// The max function separates this into two cases. The case to take is
// not clear yet, because we don't know Ao, but later we see a trick
// how to determine which case is the one to take.
// first case: Ai(Ao) <= Ao:
// Therefore, A = max(Ai(Ao), Ao) = Ao, we find Ao = A.
// This also leads to Ai(A) <= A by substitution into the condition.
// second case: Ai(Ao) > Ao:
// Therefore, A = max(Ai(Ao), Ao) = Ai(Ao) = m*Ao + n. Solving for Ao
// gives Ao = (A - n)/m.
//
// We know
// Ai(Ao) > Ao <=> A = Ai(Ao) > Ao = (A - n)/m,
// so A > (A - n)/m.
//
// **Assuming m > 0**, by multiplying with m, we can transform this to
// A * m + n > A.
//
// We know Ai(A) = A*m + n, therefore Ai(A) > A.
//
// This means that if we apply the incoming amount calculation to the
// **incoming** amount, and this condition holds, then we know that we
// deal with the second case, being able to compute the outgoing amount
// based off the formula Ao = (A - n)/m, otherwise we will just return
// the incoming amount.
// In case the inbound fee rate is less than -1 (-100%), we fail to
// compute the outbound amount and return the incoming amount. This also
// protects against zero division later.
// We compute m in terms of big.Int to be safe from overflows and to be
// consistent with later calculations.
// m := (PPM*PPM + Ri*PPM + Ro*PPM + Ro*Ri)/(PPM*PPM)
// Compute terms in (PPM*PPM + Ri*PPM + Ro*PPM + Ro*Ri).
m1 := new(big.Int).Mul(PPM, PPM)
m2 := new(big.Int).Mul(Ri, PPM)
m3 := new(big.Int).Mul(Ro, PPM)
m4 := new(big.Int).Mul(Ro, Ri)
// Add up terms m1..m4.
m := big.NewInt(0)
m.Add(m, m1)
m.Add(m, m2)
m.Add(m, m3)
m.Add(m, m4)
// Since we compare to 0, we can multiply by PPM*PPM to avoid the
// division.
if m.Int64() <= 0 {
return incomingAmt
}
// In order to decide if the total fee is negative, we apply the fee
// to the *incoming* amount as mentioned before.
// We compute the test amount in terms of big.Int to be safe from
// overflows and to be consistent later calculations.
// testAmtF := A*m + n =
// = A + Bo + Bi + (PPM*(A*Ri + A*Ro + Ro*Ri) + A*Ri*Ro)/(PPM*PPM)
// Compute terms in (A*Ri + A*Ro + Ro*Ri).
t1 := new(big.Int).Mul(A, Ri)
t2 := new(big.Int).Mul(A, Ro)
t3 := new(big.Int).Mul(Ro, Ri)
// Sum up terms t1-t3.
t4 := big.NewInt(0)
t4.Add(t4, t1)
t4.Add(t4, t2)
t4.Add(t4, t3)
// Compute PPM*(A*Ri + A*Ro + Ro*Ri).
t6 := new(big.Int).Mul(PPM, t4)
// Compute A*Ri*Ro.
t7 := new(big.Int).Mul(A, Ri)
t7.Mul(t7, Ro)
// Compute (PPM*(A*Ri + A*Ro + Ro*Ri) + A*Ri*Ro)/(PPM*PPM).
num := new(big.Int).Add(t6, t7)
denom := new(big.Int).Mul(PPM, PPM)
fraction := new(big.Int).Div(num, denom)
// Sum up all terms.
testAmt := big.NewInt(0)
testAmt.Add(testAmt, A)
testAmt.Add(testAmt, Bo)
testAmt.Add(testAmt, Bi)
testAmt.Add(testAmt, fraction)
// Protect against negative values for the integer cast to Msat.
if testAmt.Int64() < 0 {
return incomingAmt
}
// If the second case holds, we have to compute the outgoing amount.
if lnwire.MilliSatoshi(testAmt.Int64()) > incomingAmt {
// Compute the outgoing amount by integer ceiling division. This
// precision is needed because PPM*PPM*A and other terms can
// easily overflow with int64, which happens with about
// A = 10_000 sat.
// out := (A - n) / m = numerator / denominator
// numerator := PPM*(PPM*(A - Bo - Bi) - Bo*Ri)
// denominator := PPM*(PPM + Ri + Ro) + Ri*Ro
var numerator big.Int
// Compute (A - Bo - Bi).
temp1 := new(big.Int).Sub(A, Bo)
temp2 := new(big.Int).Sub(temp1, Bi)
// Compute terms in (PPM*(A - Bo - Bi) - Bo*Ri).
temp3 := new(big.Int).Mul(PPM, temp2)
temp4 := new(big.Int).Mul(Bo, Ri)
// Compute PPM*(PPM*(A - Bo - Bi) - Bo*Ri)
temp5 := new(big.Int).Sub(temp3, temp4)
numerator.Mul(PPM, temp5)
var denominator big.Int
// Compute (PPM + Ri + Ro).
temp1 = new(big.Int).Add(PPM, Ri)
temp2 = new(big.Int).Add(temp1, Ro)
// Compute PPM*(PPM + Ri + Ro) + Ri*Ro.
temp3 = new(big.Int).Mul(PPM, temp2)
temp4 = new(big.Int).Mul(Ri, Ro)
denominator.Add(temp3, temp4)
// We overestimate the outgoing amount by taking the ceiling of
// the division. This means that we may round slightly up by a
// MilliSatoshi, but this helps to ensure that we don't hit min
// HTLC constrains in the context of finding the minimum amount
// of a route.
// ceil = floor((numerator + denominator - 1) / denominator)
ceil := new(big.Int).Add(&numerator, &denominator)
ceil.Sub(ceil, big.NewInt(1))
ceil.Div(ceil, &denominator)
return lnwire.MilliSatoshi(ceil.Int64())
}
// Otherwise the inbound fee made up for the outbound fee, which is why
// we just return the incoming amount.
return incomingAmt
}

View file

@ -24,6 +24,7 @@ import (
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/channeldb/models"
"github.com/lightningnetwork/lnd/clock"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/graph"
"github.com/lightningnetwork/lnd/htlcswitch"
"github.com/lightningnetwork/lnd/input"
@ -1536,10 +1537,10 @@ func TestBuildRoute(t *testing.T) {
}, 6),
// Create two channels from b to c. For building routes, we
// expect the lowest cost channel to be selected. Note that this
// isn't a situation that we are expecting in reality. Routing
// nodes are recommended to keep their channel policies towards
// the same peer identical.
// expect the highest cost channel to be selected. Note that
// this isn't a situation that we are expecting in reality.
// Routing nodes are recommended to keep their channel policies
// towards the same peer identical.
symmetricTestChannel("b", "c", chanCapSat, &testChannelPolicy{
Expiry: 144,
FeeRate: 50000,
@ -1555,6 +1556,8 @@ func TestBuildRoute(t *testing.T) {
Features: paymentAddrFeatures,
}, 7),
// Create some channels that have conflicting min/max
// constraints.
symmetricTestChannel("a", "e", chanCapSat, &testChannelPolicy{
Expiry: 144,
FeeRate: 80000,
@ -1569,9 +1572,50 @@ func TestBuildRoute(t *testing.T) {
MaxHTLC: lnwire.NewMSatFromSatoshis(chanCapSat),
Features: paymentAddrFeatures,
}, 4),
// Create some channels that have a conflicting max HTLC
// constraint for one node pair, similar to the b->c channels.
symmetricTestChannel("b", "z", chanCapSat, &testChannelPolicy{
Expiry: 144,
FeeRate: 50000,
MinHTLC: lnwire.NewMSatFromSatoshis(20),
MaxHTLC: lnwire.NewMSatFromSatoshis(25),
Features: paymentAddrFeatures,
}, 3),
symmetricTestChannel("b", "z", chanCapSat, &testChannelPolicy{
Expiry: 144,
FeeRate: 60000,
MinHTLC: lnwire.NewMSatFromSatoshis(20),
MaxHTLC: lnwire.MilliSatoshi(20100),
Features: paymentAddrFeatures,
}, 8),
// Create a route with inbound fees.
symmetricTestChannel("a", "d", chanCapSat, &testChannelPolicy{
Expiry: 144,
FeeRate: 20000,
MinHTLC: lnwire.NewMSatFromSatoshis(5),
MaxHTLC: lnwire.NewMSatFromSatoshis(
chanCapSat,
),
InboundFeeBaseMsat: -1000,
InboundFeeRate: -1000,
}, 9),
symmetricTestChannel("d", "f", chanCapSat, &testChannelPolicy{
Expiry: 144,
FeeRate: 60000,
MinHTLC: lnwire.NewMSatFromSatoshis(20),
MaxHTLC: lnwire.NewMSatFromSatoshis(120),
Features: paymentAddrFeatures,
// The inbound fee will not be active for the last hop.
InboundFeeBaseMsat: 2000,
InboundFeeRate: 2000,
}, 10),
}
testGraph, err := createTestGraphFromChannels(t, true, testChannels, "a")
testGraph, err := createTestGraphFromChannels(
t, true, testChannels, "a",
)
require.NoError(t, err, "unable to create graph")
const startingBlockHeight = 101
@ -1583,15 +1627,10 @@ func TestBuildRoute(t *testing.T) {
t.Helper()
if len(rt.Hops) != len(expected) {
t.Fatal("hop count mismatch")
}
require.Len(t, rt.Hops, len(expected))
for i, hop := range rt.Hops {
if hop.ChannelID != expected[i] {
t.Fatalf("expected channel %v at pos %v, but "+
"got channel %v",
expected[i], i, hop.ChannelID)
}
require.Equal(t, expected[i], hop.ChannelID)
}
lastHop := rt.Hops[len(rt.Hops)-1]
@ -1603,101 +1642,204 @@ func TestBuildRoute(t *testing.T) {
_, err = rand.Read(payAddr[:])
require.NoError(t, err)
noAmt := fn.None[lnwire.MilliSatoshi]()
// Test that we can't build a route when no hops are given.
hops = []route.Vertex{}
_, err = ctx.router.BuildRoute(noAmt, hops, nil, 40, nil)
require.Error(t, err)
// Create hop list for an unknown destination.
hops := []route.Vertex{ctx.aliases["b"], ctx.aliases["y"]}
_, err = ctx.router.BuildRoute(noAmt, hops, nil, 40, &payAddr)
noChanErr := ErrNoChannel{}
require.ErrorAs(t, err, &noChanErr)
require.Equal(t, 1, noChanErr.position)
// Create hop list from the route node pubkeys.
hops := []route.Vertex{
ctx.aliases["b"], ctx.aliases["c"],
}
hops = []route.Vertex{ctx.aliases["b"], ctx.aliases["c"]}
amt := lnwire.NewMSatFromSatoshis(100)
// Build the route for the given amount.
rt, err := ctx.router.BuildRoute(
&amt, hops, nil, 40, &payAddr,
)
if err != nil {
t.Fatal(err)
}
rt, err := ctx.router.BuildRoute(fn.Some(amt), hops, nil, 40, &payAddr)
require.NoError(t, err)
// Check that we get the expected route back. The total amount should be
// the amount to deliver to hop c (100 sats) plus the max fee for the
// connection b->c (6 sats).
checkHops(rt, []uint64{1, 7}, payAddr)
if rt.TotalAmount != 106000 {
t.Fatalf("unexpected total amount %v", rt.TotalAmount)
}
require.Equal(t, lnwire.MilliSatoshi(106000), rt.TotalAmount)
// Build the route for the minimum amount.
rt, err = ctx.router.BuildRoute(
nil, hops, nil, 40, &payAddr,
)
if err != nil {
t.Fatal(err)
}
rt, err = ctx.router.BuildRoute(noAmt, hops, nil, 40, &payAddr)
require.NoError(t, err)
// Check that we get the expected route back. The minimum that we can
// send from b to c is 20 sats. Hop b charges 1200 msat for the
// forwarding. The channel between hop a and b can carry amounts in the
// range [5, 100], so 21200 msats is the minimum amount for this route.
checkHops(rt, []uint64{1, 7}, payAddr)
if rt.TotalAmount != 21200 {
t.Fatalf("unexpected total amount %v", rt.TotalAmount)
}
require.Equal(t, lnwire.MilliSatoshi(21200), rt.TotalAmount)
// The receiver gets sent the minimal HTLC amount.
require.Equal(t, lnwire.MilliSatoshi(20000), rt.Hops[1].AmtToForward)
// Test a route that contains incompatible channel htlc constraints.
// There is no amount that can pass through both channel 5 and 4.
hops = []route.Vertex{
ctx.aliases["e"], ctx.aliases["c"],
}
_, err = ctx.router.BuildRoute(
nil, hops, nil, 40, nil,
)
errNoChannel, ok := err.(ErrNoChannel)
if !ok {
t.Fatalf("expected incompatible policies error, but got %v",
err)
}
if errNoChannel.position != 0 {
t.Fatalf("unexpected no channel error position")
}
if errNoChannel.fromNode != ctx.aliases["a"] {
t.Fatalf("unexpected no channel error node")
}
hops = []route.Vertex{ctx.aliases["e"], ctx.aliases["c"]}
_, err = ctx.router.BuildRoute(noAmt, hops, nil, 40, nil)
require.Error(t, err)
noChanErr = ErrNoChannel{}
require.ErrorAs(t, err, &noChanErr)
require.Equal(t, 0, noChanErr.position)
// Test a route that contains channel constraints that lead to a
// different selection of a unified edge, when the amount is rescaled
// for the final edge. From a backward pass we expect the policy of
// channel 8 to be used, because its policy has the highest fee rate,
// bumping the amount to 20000 msat leading to a sender amount of 21200
// msat including the fees for hop over channel 8. In the forward pass
// however, we subtract that fee again, resulting in the min HTLC
// amount. The forward pass doesn't check for a different policy that
// could me more applicable, which is why we don't get back the highest
// amount that could be delivered to the receiver of 21819 msat, using
// policy of channel 3.
hops = []route.Vertex{ctx.aliases["b"], ctx.aliases["z"]}
rt, err = ctx.router.BuildRoute(noAmt, hops, nil, 40, &payAddr)
require.NoError(t, err)
checkHops(rt, []uint64{1, 8}, payAddr)
require.Equal(t, lnwire.MilliSatoshi(21200), rt.TotalAmount)
require.Equal(t, lnwire.MilliSatoshi(20000), rt.Hops[1].AmtToForward)
// Check that we compute a correct forwarding amount that involves
// inbound fees. We expect a similar amount as for the above case of
// b->c, but reduced by the inbound discount on the channel a->d.
// We get 106000 - 1000 (base in) - 0.001 * 106000 (rate in) = 104894.
hops = []route.Vertex{ctx.aliases["d"], ctx.aliases["f"]}
amt = lnwire.NewMSatFromSatoshis(100)
rt, err = ctx.router.BuildRoute(fn.Some(amt), hops, nil, 40, &payAddr)
require.NoError(t, err)
checkHops(rt, []uint64{9, 10}, payAddr)
require.EqualValues(t, 104894, rt.TotalAmount)
// Also check the min amount with inbound fees. The min amount bumps
// this to 20000 msat for the last hop. The outbound fee is 1200 msat,
// the inbound fee is -1021.2 msat (rounded down). This results in a
// total fee of 179 msat, giving a sender amount of 20179 msat. The
// determined receiver amount however reduces this to 20001 msat again
// due to rounding. This would not be compatible with the sender amount
// of 20179 msat, which results in underpayment of 1 msat in fee. There
// is a third pass through newRoute in which this gets corrected to end
hops = []route.Vertex{ctx.aliases["d"], ctx.aliases["f"]}
rt, err = ctx.router.BuildRoute(noAmt, hops, nil, 40, &payAddr)
require.NoError(t, err)
checkHops(rt, []uint64{9, 10}, payAddr)
require.EqualValues(t, 20180, rt.TotalAmount, "%v", rt.TotalAmount)
}
// TestGetPathEdges tests that the getPathEdges function returns the expected
// edges and amount when given a set of unifiers and does not panic.
func TestGetPathEdges(t *testing.T) {
// TestReceiverAmtForwardPass tests that the forward pass returns the expected
// receiver amount when given a set of edges and does not panic.
func TestReceiverAmtForwardPass(t *testing.T) {
t.Parallel()
const startingBlockHeight = 101
ctx := createTestCtxFromFile(t, startingBlockHeight, basicGraphFilePath)
testCases := []struct {
sourceNode route.Vertex
amt lnwire.MilliSatoshi
unifiers []*edgeUnifier
bandwidthHints *bandwidthManager
hops []route.Vertex
name string
amt lnwire.MilliSatoshi
unifiedEdges []*unifiedEdge
hops []route.Vertex
expectedEdges []*models.CachedEdgePolicy
expectedAmt lnwire.MilliSatoshi
expectedErr string
}{{
sourceNode: ctx.aliases["roasbeef"],
unifiers: []*edgeUnifier{
{
edges: []*unifiedEdge{},
localChan: true,
},
expectedAmt lnwire.MilliSatoshi
expectedErr string
}{
{
name: "empty",
expectedErr: "no edges to forward through",
},
expectedErr: fmt.Sprintf("no matching outgoing channel "+
"available for node 0 (%v)", ctx.aliases["roasbeef"]),
}}
{
name: "single edge, no valid policy",
amt: 1000,
unifiedEdges: []*unifiedEdge{
{
policy: &models.CachedEdgePolicy{
MinHTLC: 1001,
},
},
},
expectedErr: fmt.Sprintf("no matching outgoing " +
"channel available for node index 0"),
},
{
name: "single edge",
amt: 1000,
unifiedEdges: []*unifiedEdge{
{
policy: &models.CachedEdgePolicy{
MinHTLC: 1000,
},
},
},
expectedAmt: 1000,
},
{
name: "outbound fee, no rounding",
amt: 1e9,
unifiedEdges: []*unifiedEdge{
{
// The first hop's outbound fee is
// irrelevant in fee calculation.
policy: &models.CachedEdgePolicy{
FeeBaseMSat: 1234,
FeeProportionalMillionths: 1234,
},
},
{
// No rounding is done here.
policy: &models.CachedEdgePolicy{
FeeBaseMSat: 1000,
FeeProportionalMillionths: 1000,
},
},
},
// From an outgoing amount of 999000000 msat, we get
// in = out + base + out * rate = 1000000000.0
//
// The inverse outgoing amount for this is
// out = (in - base) / (1 + rate) =
// (1e9 - 1000) / (1 + 1e-3) = 999000000.0000001,
// which is rounded down.
expectedAmt: 999000000,
},
{
name: "outbound fee, rounding",
amt: 1e9,
unifiedEdges: []*unifiedEdge{
{
// The first hop's outbound fee is
// irrelevant in fee calculation.
policy: &models.CachedEdgePolicy{
FeeBaseMSat: 1234,
FeeProportionalMillionths: 1234,
},
},
{
// This policy is chosen such that we
// round down.
policy: &models.CachedEdgePolicy{
FeeBaseMSat: 1000,
FeeProportionalMillionths: 999,
},
},
},
// The float amount for this is
// out = (in - base) / (1 + rate) =
// (1e9 - 1000) / (1 + 999e-6) = 999000998.002995,
// which is rounded up.
expectedAmt: 999000999,
},
}
for _, tc := range testCases {
pathEdges, amt, err := getPathEdges(
tc.sourceNode, tc.amt, tc.unifiers, tc.bandwidthHints,
tc.hops,
)
amt, err := receiverAmtForwardPass(tc.amt, tc.unifiedEdges)
if tc.expectedErr != "" {
require.Error(t, err)
@ -1707,11 +1849,285 @@ func TestGetPathEdges(t *testing.T) {
}
require.NoError(t, err)
require.Equal(t, pathEdges, tc.expectedEdges)
require.Equal(t, amt, tc.expectedAmt)
}
}
// TestSenderAmtBackwardPass tests that the computation of the sender amount is
// done correctly for route building.
func TestSenderAmtBackwardPass(t *testing.T) {
bandwidthHints := bandwidthManager{
getLink: func(chanId lnwire.ShortChannelID) (
htlcswitch.ChannelLink, error) {
return nil, nil
},
localChans: make(map[lnwire.ShortChannelID]struct{}),
}
var (
capacity btcutil.Amount = 1_000_000
testReceiverAmt lnwire.MilliSatoshi = 1_000_000
minHTLC lnwire.MilliSatoshi = 1_000
)
edgeUnifiers := []*edgeUnifier{
{
edges: []*unifiedEdge{
{
// This outbound fee doesn't have an
// effect (sender doesn't pay outbound).
policy: &models.CachedEdgePolicy{
FeeBaseMSat: 112,
},
inboundFees: models.InboundFee{
Base: 111,
},
capacity: capacity,
},
},
},
{
edges: []*unifiedEdge{
{
policy: &models.CachedEdgePolicy{
FeeBaseMSat: 222,
},
inboundFees: models.InboundFee{
Base: 222,
},
capacity: capacity,
},
},
},
{
edges: []*unifiedEdge{
{
policy: &models.CachedEdgePolicy{
FeeBaseMSat: 333,
MinHTLC: minHTLC,
},
// In pathfinding, inbound fees are not
// populated for exit hops because the
// newNodeEdgeUnifier enforces this.
// This is important as otherwise we
// would not fail the min HTLC check in
// getEdge.
capacity: capacity,
},
},
},
}
// A search for an amount that is below the minimum HTLC amount should
// fail.
_, _, err := senderAmtBackwardPass(
edgeUnifiers, fn.Some(minHTLC-1), &bandwidthHints,
)
require.Error(t, err)
// Do a min amount search.
_, senderAmount, err := senderAmtBackwardPass(
edgeUnifiers, fn.None[lnwire.MilliSatoshi](), &bandwidthHints,
)
require.NoError(t, err)
require.Equal(t, minHTLC+333+222+222+111, senderAmount)
// Do a search for a specific amount.
unifiedEdges, senderAmount, err := senderAmtBackwardPass(
edgeUnifiers, fn.Some(testReceiverAmt), &bandwidthHints,
)
require.NoError(t, err)
require.Equal(t, testReceiverAmt+333+222+222+111, senderAmount)
// Check that we arrive at the same receiver amount by doing a forward
// pass.
receiverAmt, err := receiverAmtForwardPass(senderAmount, unifiedEdges)
require.NoError(t, err)
require.Equal(t, testReceiverAmt, receiverAmt)
// Insert a policy that leads to rounding.
edgeUnifiers[1] = &edgeUnifier{
edges: []*unifiedEdge{
{
policy: &models.CachedEdgePolicy{
FeeBaseMSat: 20,
FeeProportionalMillionths: 100,
},
inboundFees: models.InboundFee{
Base: -10,
Rate: -50,
},
capacity: capacity,
},
},
}
unifiedEdges, senderAmount, err = senderAmtBackwardPass(
edgeUnifiers, fn.Some(testReceiverAmt), &bandwidthHints,
)
require.NoError(t, err)
// For this route, we have some rounding errors, so we can't expect the
// exact amount, but it should be higher than the exact amount, to not
// end up below a min HTLC constraint.
receiverAmt, err = receiverAmtForwardPass(senderAmount, unifiedEdges)
require.NoError(t, err)
require.NotEqual(t, testReceiverAmt, receiverAmt)
require.InDelta(t, int64(testReceiverAmt), int64(receiverAmt), 1)
require.GreaterOrEqual(t, int64(receiverAmt), int64(testReceiverAmt))
}
// TestInboundOutbound tests the functions that computes the incoming and
// outgoing amounts based on the fees of the incoming and outgoing channels.
func TestInboundOutbound(t *testing.T) {
var outgoingAmt uint64 = 10_000_000
tests := []struct {
name string
incomingBase int32
incomingRate int32
outgoingBase uint64
outgoingRate uint64
}{
{
name: "only outbound fee",
incomingBase: 0,
incomingRate: 0,
outgoingBase: 20,
outgoingRate: 100,
},
{
name: "positive inbound and outbound fee",
incomingBase: 20,
incomingRate: 100,
outgoingBase: 20,
outgoingRate: 100,
},
{
name: "small negative inbound and outbound fee",
incomingBase: -10,
incomingRate: -50,
outgoingBase: 20,
outgoingRate: 100,
},
{
name: "equal negative inbound and outbound fee",
incomingBase: -20,
incomingRate: -100,
outgoingBase: 20,
outgoingRate: 100,
},
{
name: "large negative inbound and outbound fee",
incomingBase: -30,
incomingRate: -200,
outgoingBase: 20,
outgoingRate: 100,
},
{
name: "order of PPM negative inbound and " +
"outbound fee (m=0)",
incomingBase: -30,
incomingRate: -1_000_000,
outgoingBase: 20,
outgoingRate: 100,
},
{
name: "huge negative inbound and " +
"outbound fee (m<0)",
incomingBase: -30,
incomingRate: -2_000_000,
outgoingBase: 20,
outgoingRate: 100,
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(tt *testing.T) {
testInboundOutboundFee(
tt, outgoingAmt, tc.incomingBase,
tc.incomingRate, tc.outgoingBase,
tc.outgoingRate,
)
})
}
}
// testInboundOutboundFee is a helper function that tests the outgoing and
// incoming amount relationship.
func testInboundOutboundFee(t *testing.T, outgoingAmt uint64, inBase,
inRate int32, outBase, outRate uint64) {
debugStr := fmt.Sprintf(
"outAmt=%d, inBase=%d, inRate=%d, outBase=%d, outRate=%d",
outgoingAmt, inBase, inRate, outBase, outRate,
)
incomingEdge := &unifiedEdge{
policy: &models.CachedEdgePolicy{},
inboundFees: models.InboundFee{
Base: inBase,
Rate: inRate,
},
}
outgoingEdge := &unifiedEdge{
policy: &models.CachedEdgePolicy{
FeeBaseMSat: lnwire.MilliSatoshi(
outBase,
),
FeeProportionalMillionths: lnwire.MilliSatoshi(
outRate,
),
},
}
// We compute the incoming amount based on the outgoing amount, which
// mimicks the path finding process.
incomingAmt := incomingFromOutgoing(
lnwire.MilliSatoshi(outgoingAmt), incomingEdge,
outgoingEdge,
)
// We do the reverse and compute the outgoing amount based on the
// incoming amount.
outgoingAmtNew := outgoingFromIncoming(
incomingAmt, incomingEdge, outgoingEdge,
)
// We require that the incoming amount is always larger than or equal to
// the outgoing amount, because total fees (=incoming-outgoing) should
// not become negative.
require.GreaterOrEqual(
t, int64(incomingAmt), int64(outgoingAmtNew), debugStr,
"expected incomingAmt >= outgoingAmtNew",
)
// We check that up to rounding the amounts are equal.
require.InDelta(
t, int64(outgoingAmt), int64(outgoingAmtNew), 1.0, debugStr,
"expected |outgoingAmt - outgoingAmtNew | <= 1",
)
// If we round, the computed outgoing amount should be larger than the
// exact outgoing amount, to not hit any min HTLC limits.
require.GreaterOrEqual(
t, int64(outgoingAmtNew), int64(outgoingAmt), debugStr,
"expected outgoingAmtNew >= outgoingAmt",
)
}
// FuzzInboundOutbound tests the incoming and outgoing amount calculation
// functions with fuzzing.
func FuzzInboundOutboundFee(f *testing.F) {
f.Add(uint64(0), int32(0), int32(0), uint64(0), uint64(0))
f.Fuzz(testInboundOutboundFee)
}
// TestSendToRouteSkipTempErrSuccess validates a successful payment send.
func TestSendToRouteSkipTempErrSuccess(t *testing.T) {
t.Parallel()