routing: backward pass determines unified edges

We shift the duty of determining the policies to the backward pass as
the forward pass will only be responsible for finding the corrected
receiver amount.

Note that this is not a pure refactor as demonstrated in the test, as
the forward pass doesn't select new policies anymore, which is less
flexible and doesn't lead to the highest possible receiver amount. This
is however neccessary as we otherwise won't be able to compute
forwarding amounts involving inbound fees and this edge case is unlikely
to occur, because we search for a min amount for a route that was most
likely constructed for a larger amount.
This commit is contained in:
bitromortac 2024-06-25 11:45:36 +02:00
parent e4c7c10618
commit c18694ff5e
No known key found for this signature in database
GPG Key ID: 1965063FC13BEBE2
2 changed files with 215 additions and 68 deletions

View File

@ -1451,35 +1451,29 @@ func (r *ChannelRouter) BuildRoute(amt *lnwire.MilliSatoshi,
// the destination. There are nodes in the wild that have a // the destination. There are nodes in the wild that have a
// min_htlc channel policy of zero, which could lead to a zero // min_htlc channel policy of zero, which could lead to a zero
// amount payment being made. // amount payment being made.
senderAmt, err := senderAmtBackwardPass( var senderAmt lnwire.MilliSatoshi
pathEdges, senderAmt, err = senderAmtBackwardPass(
unifiers, useMinAmt, 1, bandwidthHints, unifiers, useMinAmt, 1, bandwidthHints,
) )
if err != nil { if err != nil {
return nil, err return nil, err
} }
pathEdges, receiverAmt, err = getPathEdges( receiverAmt, err = receiverAmtForwardPass(senderAmt, pathEdges)
senderAmt, unifiers, bandwidthHints,
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} else { } else {
// If an amount is specified, we need to build a route that // If an amount is specified, we need to build a route that
// delivers exactly this amount to the final destination. // delivers exactly this amount to the final destination.
senderAmt, err := senderAmtBackwardPass( pathEdges, _, err = senderAmtBackwardPass(
unifiers, useMinAmt, *amt, bandwidthHints, unifiers, useMinAmt, *amt, bandwidthHints,
) )
if err != nil { if err != nil {
return nil, err return nil, err
} }
pathEdges, receiverAmt, err = getPathEdges( receiverAmt = *amt
senderAmt, unifiers, bandwidthHints,
)
if err != nil {
return nil, err
}
} }
// Fetch the current block height outside the routing transaction, to // Fetch the current block height outside the routing transaction, to
@ -1547,12 +1541,15 @@ func getEdgeUnifiers(source route.Vertex, hops []route.Vertex,
return unifiers, nil return unifiers, nil
} }
// senderAmtBackwardPass determines the amount that should be sent to fulfill // senderAmtBackwardPass returns a list of unified edges for the given route and
// min HTLC requirements. The minimal sender amount can be searched for by // determines the amount that should be sent to fulfill min HTLC requirements.
// activating useMinAmt. // The minimal sender amount can be searched for by activating useMinAmt.
func senderAmtBackwardPass(unifiers []*edgeUnifier, useMinAmt bool, func senderAmtBackwardPass(unifiers []*edgeUnifier, useMinAmt bool,
runningAmt lnwire.MilliSatoshi, runningAmt lnwire.MilliSatoshi,
bandwidthHints *bandwidthManager) (lnwire.MilliSatoshi, error) { bandwidthHints bandwidthHints) ([]*unifiedEdge, lnwire.MilliSatoshi,
error) {
var unifiedEdges = make([]*unifiedEdge, len(unifiers))
// Traverse hops backwards to accumulate fees in the running amounts. // Traverse hops backwards to accumulate fees in the running amounts.
for i := len(unifiers) - 1; i >= 0; i-- { for i := len(unifiers) - 1; i >= 0; i-- {
@ -1573,7 +1570,7 @@ func senderAmtBackwardPass(unifiers []*edgeUnifier, useMinAmt bool,
log.Errorf("Cannot find policy with amt=%v for hop %v", log.Errorf("Cannot find policy with amt=%v for hop %v",
runningAmt, i) runningAmt, i)
return 0, ErrNoChannel{ return nil, 0, ErrNoChannel{
position: i, position: i,
} }
} }
@ -1585,37 +1582,37 @@ func senderAmtBackwardPass(unifiers []*edgeUnifier, useMinAmt bool,
log.Tracef("Select channel %v at position %v", log.Tracef("Select channel %v at position %v",
edge.policy.ChannelID, i) edge.policy.ChannelID, i)
unifiedEdges[i] = edge
} }
return runningAmt, nil return unifiedEdges, runningAmt, nil
} }
// getPathEdges returns the edges that make up the path and the total amount, // receiverAmtForwardPass returns the amount that a receiver will receive after
// including fees, to send the payment. // deducting all fees from the sender amount.
func getPathEdges(receiverAmt lnwire.MilliSatoshi, unifiers []*edgeUnifier, func receiverAmtForwardPass(runningAmt lnwire.MilliSatoshi,
bandwidthHints *bandwidthManager) ([]*unifiedEdge, lnwire.MilliSatoshi, unifiedEdges []*unifiedEdge) (lnwire.MilliSatoshi, error) {
error) {
// Now that we arrived at the start of the route and found out the route // 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 // total amount, we make a forward pass. Because the amount may have
// been increased in the backward pass, fees need to be recalculated and // been increased in the backward pass, fees need to be recalculated and
// amount ranges re-checked. // amount ranges re-checked.
var pathEdges []*unifiedEdge for i, edge := range unifiedEdges {
for i, unifier := range unifiers {
edge := unifier.getEdge(receiverAmt, bandwidthHints, 0)
if edge == nil {
return nil, 0, ErrNoChannel{position: i}
}
if i > 0 { if i > 0 {
// Decrease the amount to send while going forward. // Decrease the amount to send while going forward.
receiverAmt -= edge.policy.ComputeFeeFromIncoming( runningAmt -= edge.policy.ComputeFeeFromIncoming(
receiverAmt, runningAmt,
) )
} }
pathEdges = append(pathEdges, edge) if !edge.amtInRange(runningAmt) {
log.Errorf("Amount %v not in range for hop %v",
runningAmt, i)
return 0, ErrNoChannel{position: i}
}
} }
return pathEdges, receiverAmt, nil return runningAmt, nil
} }

View File

@ -1669,53 +1669,122 @@ func TestBuildRoute(t *testing.T) {
// channel 8 to be used, because its policy has the highest fee rate, // 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 // 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 // msat including the fees for hop over channel 8. In the forward pass
// however, we discover that the max HTLC constraint of channel 8 is // however, we subtract that fee again, resulting in the min HTLC
// violated after subtracting the fee, which is why we change the policy // amount. The forward pass doesn't check for a different policy that
// to the one of channel 3. This leads to a delivered amount of 20191 // could me more applicable, which is why we don't get back the highest
// msat, in contrast to the naive 20000 msat from the min HTLC // amount that could be delivered to the receiver of 21819 msat, using
// constraint of both channels. // policy of channel 3.
hops = []route.Vertex{ctx.aliases["b"], ctx.aliases["z"]} hops = []route.Vertex{ctx.aliases["b"], ctx.aliases["z"]}
rt, err = ctx.router.BuildRoute(nil, hops, nil, 40, &payAddr) rt, err = ctx.router.BuildRoute(nil, hops, nil, 40, &payAddr)
require.NoError(t, err) require.NoError(t, err)
checkHops(rt, []uint64{1, 3}, payAddr) checkHops(rt, []uint64{1, 8}, payAddr)
require.Equal(t, lnwire.MilliSatoshi(21200), rt.TotalAmount) require.Equal(t, lnwire.MilliSatoshi(21200), rt.TotalAmount)
require.Equal(t, lnwire.MilliSatoshi(20191), rt.Hops[1].AmtToForward) require.Equal(t, lnwire.MilliSatoshi(20000), rt.Hops[1].AmtToForward)
} }
// TestGetPathEdges tests that the getPathEdges function returns the expected // TestReceiverAmtForwardPass tests that the forward pass returns the expected
// edges and amount when given a set of unifiers and does not panic. // receiver amount when given a set of edges and does not panic.
func TestGetPathEdges(t *testing.T) { func TestReceiverAmtForwardPass(t *testing.T) {
t.Parallel() t.Parallel()
const startingBlockHeight = 101
ctx := createTestCtxFromFile(t, startingBlockHeight, basicGraphFilePath)
testCases := []struct { testCases := []struct {
sourceNode route.Vertex name string
amt lnwire.MilliSatoshi amt lnwire.MilliSatoshi
unifiers []*edgeUnifier unifiedEdges []*unifiedEdge
bandwidthHints *bandwidthManager hops []route.Vertex
hops []route.Vertex
expectedEdges []*models.CachedEdgePolicy expectedAmt lnwire.MilliSatoshi
expectedAmt lnwire.MilliSatoshi expectedErr string
expectedErr string }{
}{{ {
sourceNode: ctx.aliases["roasbeef"], name: "empty",
unifiers: []*edgeUnifier{
{
edges: []*unifiedEdge{},
localChan: true,
},
}, },
expectedErr: fmt.Sprintf("no matching outgoing channel " + {
"available for node index 0"), 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 { for _, tc := range testCases {
pathEdges, amt, err := getPathEdges( amt, err := receiverAmtForwardPass(tc.amt, tc.unifiedEdges)
tc.amt, tc.unifiers, tc.bandwidthHints,
)
if tc.expectedErr != "" { if tc.expectedErr != "" {
require.Error(t, err) require.Error(t, err)
@ -1725,11 +1794,92 @@ func TestGetPathEdges(t *testing.T) {
} }
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, pathEdges, tc.expectedEdges)
require.Equal(t, amt, tc.expectedAmt) 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,
},
capacity: capacity,
},
},
},
{
edges: []*unifiedEdge{
{
policy: &models.CachedEdgePolicy{
FeeBaseMSat: 222,
},
capacity: capacity,
},
},
},
{
edges: []*unifiedEdge{
{
policy: &models.CachedEdgePolicy{
FeeBaseMSat: 333,
MinHTLC: minHTLC,
},
capacity: capacity,
},
},
},
}
// A search for an amount that is below the minimum HTLC amount should
// fail.
_, _, err := senderAmtBackwardPass(
edgeUnifiers, false, minHTLC-1, &bandwidthHints,
)
require.Error(t, err)
// Do a min amount search.
unifiedEdges, senderAmount, err := senderAmtBackwardPass(
edgeUnifiers, true, 1, &bandwidthHints,
)
require.NoError(t, err)
require.Equal(t, minHTLC+333+222, senderAmount)
// Do a search for a specific amount.
unifiedEdges, senderAmount, err = senderAmtBackwardPass(
edgeUnifiers, false, testReceiverAmt, &bandwidthHints,
)
require.NoError(t, err)
require.Equal(t, testReceiverAmt+333+222, 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)
}
// TestSendToRouteSkipTempErrSuccess validates a successful payment send. // TestSendToRouteSkipTempErrSuccess validates a successful payment send.
func TestSendToRouteSkipTempErrSuccess(t *testing.T) { func TestSendToRouteSkipTempErrSuccess(t *testing.T) {
t.Parallel() t.Parallel()