diff --git a/cmd/lncli/cmd_mission_control.go b/cmd/lncli/cmd_mission_control.go index e26481b23..9d8b5533b 100644 --- a/cmd/lncli/cmd_mission_control.go +++ b/cmd/lncli/cmd_mission_control.go @@ -163,9 +163,10 @@ func queryMissionControl(ctx *cli.Context) error { var queryProbCommand = cli.Command{ Name: "queryprob", Category: "Mission Control", - Usage: "Estimate a success probability.", + Usage: "Deprecated. Estimate a success probability.", ArgsUsage: "from-node to-node amt", Action: actionDecorator(queryProb), + Hidden: true, } func queryProb(ctx *cli.Context) error { diff --git a/docs/release-notes/release-notes-0.16.0.md b/docs/release-notes/release-notes-0.16.0.md index 0f14bc3e5..802e32815 100644 --- a/docs/release-notes/release-notes-0.16.0.md +++ b/docs/release-notes/release-notes-0.16.0.md @@ -77,6 +77,9 @@ GetBlock, GetBestBlock, GetBlockHash. These endpoints provide access to chain block data. +* [`QueryProbabiltiy` is deprecated. Internal mission control state can be + obtained via `QueryMissionControl`.]( + https://github.com/lightningnetwork/lnd/pull/6857) ## Wallet @@ -260,6 +263,11 @@ certain large transactions](https://github.com/lightningnetwork/lnd/pull/7100). using the address in question]( https://github.com/lightningnetwork/lnd/pull/7025) +## Pathfinding + +* [Pathfinding takes capacity of edges into account to improve success + probability estimation.](https://github.com/lightningnetwork/lnd/pull/6857) + ### Tooling and documentation * [The `golangci-lint` tool was updated to diff --git a/go.mod b/go.mod index 048f0de24..65425bae2 100644 --- a/go.mod +++ b/go.mod @@ -49,9 +49,10 @@ require ( go.etcd.io/etcd/client/pkg/v3 v3.5.0 go.etcd.io/etcd/client/v3 v3.5.0 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 - golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f + golang.org/x/exp v0.0.0-20221111094246-ab4555d3164f + golang.org/x/net v0.1.0 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c - golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 + golang.org/x/term v0.1.0 golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba google.golang.org/grpc v1.38.0 google.golang.org/protobuf v1.26.0 @@ -144,9 +145,8 @@ require ( go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.6.0 // indirect go.uber.org/zap v1.17.0 // indirect - golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect - golang.org/x/text v0.3.7 // indirect - golang.org/x/tools v0.1.8 // indirect + golang.org/x/sys v0.1.0 // indirect + golang.org/x/text v0.4.0 // indirect google.golang.org/genproto v0.0.0-20210617175327-b9e0b3197ced // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/errgo.v1 v1.0.1 // indirect diff --git a/go.sum b/go.sum index 1c4856482..39300be85 100644 --- a/go.sum +++ b/go.sum @@ -270,8 +270,8 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -728,6 +728,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20221111094246-ab4555d3164f h1:dx+ByaHZgQORWfy/TumeLerCm48o5pDf+skF0SWtLdY= +golang.org/x/exp v0.0.0-20221111094246-ab4555d3164f/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -794,8 +796,9 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f h1:OfiFi4JbukWwe3lzw+xunroH1mnC1e2Gy5cxNJApiSY= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -875,11 +878,13 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -888,8 +893,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -948,8 +953,7 @@ golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.8 h1:P1HhGGuLW4aAclzjtmJdf0mJOjVUZUzOTqkAkWL+l6w= -golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= +golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/lnrpc/routerrpc/router.proto b/lnrpc/routerrpc/router.proto index 4cfd4f37f..cf0de130b 100644 --- a/lnrpc/routerrpc/router.proto +++ b/lnrpc/routerrpc/router.proto @@ -94,8 +94,8 @@ service Router { returns (SetMissionControlConfigResponse); /* - QueryProbability returns the current success probability estimate for a - given node pair and amount. + Deprecated. QueryProbability returns the current success probability + estimate for a given node pair and amount. */ rpc QueryProbability (QueryProbabilityRequest) returns (QueryProbabilityResponse); diff --git a/lnrpc/routerrpc/router.swagger.json b/lnrpc/routerrpc/router.swagger.json index 7cd634a87..1503f2a15 100644 --- a/lnrpc/routerrpc/router.swagger.json +++ b/lnrpc/routerrpc/router.swagger.json @@ -116,7 +116,7 @@ }, "/v2/router/mc/probability/{from_node}/{to_node}/{amt_msat}": { "get": { - "summary": "QueryProbability returns the current success probability estimate for a\ngiven node pair and amount.", + "summary": "Deprecated. QueryProbability returns the current success probability\nestimate for a given node pair and amount.", "operationId": "Router_QueryProbability", "responses": { "200": { diff --git a/lnrpc/routerrpc/router_backend.go b/lnrpc/routerrpc/router_backend.go index 431a650ce..58b7a8294 100644 --- a/lnrpc/routerrpc/router_backend.go +++ b/lnrpc/routerrpc/router_backend.go @@ -45,6 +45,11 @@ type RouterBackend struct { // capacity of a channel to populate in responses. FetchChannelCapacity func(chanID uint64) (btcutil.Amount, error) + // FetchAmountPairCapacity determines the maximal channel capacity + // between two nodes given a certain amount. + FetchAmountPairCapacity func(nodeFrom, nodeTo route.Vertex, + amount lnwire.MilliSatoshi) (btcutil.Amount, error) + // FetchChannelEndpoints returns the pubkeys of both endpoints of the // given channel id. FetchChannelEndpoints func(chanID uint64) (route.Vertex, @@ -57,7 +62,7 @@ type RouterBackend struct { restrictions *routing.RestrictParams, destCustomRecords record.CustomSet, routeHints map[route.Vertex][]*channeldb.CachedEdgePolicy, - finalExpiry uint16) (*route.Route, error) + finalExpiry uint16) (*route.Route, float64, error) MissionControl MissionControl @@ -103,7 +108,7 @@ type MissionControl interface { // GetProbability is expected to return the success probability of a // payment from fromNode to toNode. GetProbability(fromNode, toNode route.Vertex, - amt lnwire.MilliSatoshi) float64 + amt lnwire.MilliSatoshi, capacity btcutil.Amount) float64 // ResetHistory resets the history of MissionControl returning it to a // state as if no payment attempts have been made. @@ -258,7 +263,8 @@ func (r *RouterBackend) QueryRoutes(ctx context.Context, restrictions := &routing.RestrictParams{ FeeLimit: feeLimit, ProbabilitySource: func(fromNode, toNode route.Vertex, - amt lnwire.MilliSatoshi) float64 { + amt lnwire.MilliSatoshi, + capacity btcutil.Amount) float64 { if _, ok := ignoredNodes[fromNode]; ok { return 0 @@ -277,7 +283,7 @@ func (r *RouterBackend) QueryRoutes(ctx context.Context, } return r.MissionControl.GetProbability( - fromNode, toNode, amt, + fromNode, toNode, amt, capacity, ) }, DestCustomRecords: record.CustomSet(in.DestCustomRecords), @@ -324,7 +330,7 @@ func (r *RouterBackend) QueryRoutes(ctx context.Context, // Query the channel router for a possible path to the destination that // can carry `in.Amt` satoshis _including_ the total fee required on // the route. - route, err := r.FindRoute( + route, successProb, err := r.FindRoute( sourcePubKey, targetPubKey, amt, in.TimePref, restrictions, customRecords, routeHintEdges, finalCLTVDelta, ) @@ -339,11 +345,6 @@ func (r *RouterBackend) QueryRoutes(ctx context.Context, return nil, err } - // Calculate route success probability. Do not rely on a probability - // that could have been returned from path finding, because mission - // control may have been disabled in the provided ProbabilitySource. - successProb := r.getSuccessProbability(route) - routeResp := &lnrpc.QueryRoutesResponse{ Routes: []*lnrpc.Route{rpcRoute}, SuccessProb: successProb, @@ -352,28 +353,6 @@ func (r *RouterBackend) QueryRoutes(ctx context.Context, return routeResp, nil } -// getSuccessProbability returns the success probability for the given route -// based on the current state of mission control. -func (r *RouterBackend) getSuccessProbability(rt *route.Route) float64 { - fromNode := rt.SourcePubKey - amtToFwd := rt.TotalAmount - successProb := 1.0 - for _, hop := range rt.Hops { - toNode := hop.PubKeyBytes - - probability := r.MissionControl.GetProbability( - fromNode, toNode, amtToFwd, - ) - - successProb *= probability - - amtToFwd = hop.AmtToForward - fromNode = toNode - } - - return successProb -} - // rpcEdgeToPair looks up the provided channel and returns the channel endpoints // as a directed pair. func (r *RouterBackend) rpcEdgeToPair(e *lnrpc.EdgeLocator) ( diff --git a/lnrpc/routerrpc/router_backend_test.go b/lnrpc/routerrpc/router_backend_test.go index a5b629fe2..4a5d73135 100644 --- a/lnrpc/routerrpc/router_backend_test.go +++ b/lnrpc/routerrpc/router_backend_test.go @@ -126,7 +126,7 @@ func testQueryRoutes(t *testing.T, useMissionControl bool, useMsat bool, amt lnwire.MilliSatoshi, _ float64, restrictions *routing.RestrictParams, _ record.CustomSet, routeHints map[route.Vertex][]*channeldb.CachedEdgePolicy, - finalExpiry uint16) (*route.Route, error) { + finalExpiry uint16) (*route.Route, float64, error) { if int64(amt) != amtSat*1000 { t.Fatal("unexpected amount") @@ -145,18 +145,18 @@ func testQueryRoutes(t *testing.T, useMissionControl bool, useMsat bool, } if restrictions.ProbabilitySource(route.Vertex{2}, - route.Vertex{1}, 0, + route.Vertex{1}, 0, 0, ) != 0 { t.Fatal("expecting 0% probability for ignored edge") } if restrictions.ProbabilitySource(ignoreNodeVertex, - route.Vertex{6}, 0, + route.Vertex{6}, 0, 0, ) != 0 { t.Fatal("expecting 0% probability for ignored node") } - if restrictions.ProbabilitySource(node1, node2, 0) != 0 { + if restrictions.ProbabilitySource(node1, node2, 0, 0) != 0 { t.Fatal("expecting 0% probability for ignored pair") } @@ -181,13 +181,15 @@ func testQueryRoutes(t *testing.T, useMissionControl bool, useMsat bool, expectedProb = testMissionControlProb } if restrictions.ProbabilitySource(route.Vertex{4}, - route.Vertex{5}, 0, + route.Vertex{5}, 0, 0, ) != expectedProb { t.Fatal("expecting 100% probability") } hops := []*route.Hop{{}} - return route.NewRouteFromHops(amt, 144, source, hops) + route, err := route.NewRouteFromHops(amt, 144, source, hops) + + return route, expectedProb, err } backend := &RouterBackend{ @@ -198,6 +200,11 @@ func testQueryRoutes(t *testing.T, useMissionControl bool, useMsat bool, return 1, nil }, + FetchAmountPairCapacity: func(nodeFrom, nodeTo route.Vertex, + amount lnwire.MilliSatoshi) (btcutil.Amount, error) { + + return 1, nil + }, MissionControl: &mockMissionControl{}, FetchChannelEndpoints: func(chanID uint64) (route.Vertex, route.Vertex, error) { @@ -239,7 +246,7 @@ type mockMissionControl struct { } func (m *mockMissionControl) GetProbability(fromNode, toNode route.Vertex, - amt lnwire.MilliSatoshi) float64 { + amt lnwire.MilliSatoshi, capacity btcutil.Amount) float64 { return testMissionControlProb } diff --git a/lnrpc/routerrpc/router_grpc.pb.go b/lnrpc/routerrpc/router_grpc.pb.go index c16198cb7..9a65770a3 100644 --- a/lnrpc/routerrpc/router_grpc.pb.go +++ b/lnrpc/routerrpc/router_grpc.pb.go @@ -65,8 +65,8 @@ type RouterClient interface { // SetMissionControlConfig will set mission control's config, if the config // provided is valid. SetMissionControlConfig(ctx context.Context, in *SetMissionControlConfigRequest, opts ...grpc.CallOption) (*SetMissionControlConfigResponse, error) - // QueryProbability returns the current success probability estimate for a - // given node pair and amount. + // Deprecated. QueryProbability returns the current success probability + // estimate for a given node pair and amount. QueryProbability(ctx context.Context, in *QueryProbabilityRequest, opts ...grpc.CallOption) (*QueryProbabilityResponse, error) // BuildRoute builds a fully specified route based on a list of hop public // keys. It retrieves the relevant channel policies from the graph in order to @@ -483,8 +483,8 @@ type RouterServer interface { // SetMissionControlConfig will set mission control's config, if the config // provided is valid. SetMissionControlConfig(context.Context, *SetMissionControlConfigRequest) (*SetMissionControlConfigResponse, error) - // QueryProbability returns the current success probability estimate for a - // given node pair and amount. + // Deprecated. QueryProbability returns the current success probability + // estimate for a given node pair and amount. QueryProbability(context.Context, *QueryProbabilityRequest) (*QueryProbabilityResponse, error) // BuildRoute builds a fully specified route based on a list of hop public // keys. It retrieves the relevant channel policies from the graph in order to diff --git a/lnrpc/routerrpc/router_server.go b/lnrpc/routerrpc/router_server.go index 1967a3dd6..44335b84b 100644 --- a/lnrpc/routerrpc/router_server.go +++ b/lnrpc/routerrpc/router_server.go @@ -366,7 +366,7 @@ func (s *Server) EstimateRouteFee(ctx context.Context, // restriction for the default CLTV limit, otherwise we can find a route // that exceeds it and is useless to us. mc := s.cfg.RouterBackend.MissionControl - route, err := s.cfg.Router.FindRoute( + route, _, err := s.cfg.Router.FindRoute( s.cfg.RouterBackend.SelfNode, destNode, amtMsat, 0, &routing.RestrictParams{ FeeLimit: feeLimit, @@ -692,33 +692,6 @@ func getMsatPairValue(msatValue lnwire.MilliSatoshi, satValue) } -// QueryProbability returns the current success probability estimate for a -// given node pair and amount. -func (s *Server) QueryProbability(ctx context.Context, - req *QueryProbabilityRequest) (*QueryProbabilityResponse, error) { - - fromNode, err := route.NewVertexFromBytes(req.FromNode) - if err != nil { - return nil, err - } - - toNode, err := route.NewVertexFromBytes(req.ToNode) - if err != nil { - return nil, err - } - - amt := lnwire.MilliSatoshi(req.AmtMsat) - - mc := s.cfg.RouterBackend.MissionControl - prob := mc.GetProbability(fromNode, toNode, amt) - history := mc.GetPairHistorySnapshot(fromNode, toNode) - - return &QueryProbabilityResponse{ - Probability: prob, - History: toRPCPairData(&history), - }, nil -} - // TrackPaymentV2 returns a stream of payment state updates. The stream is // closed when the payment completes. func (s *Server) TrackPaymentV2(request *TrackPaymentRequest, diff --git a/lnrpc/routerrpc/router_server_deprecated.go b/lnrpc/routerrpc/router_server_deprecated.go index f23ebb190..0918c1e64 100644 --- a/lnrpc/routerrpc/router_server_deprecated.go +++ b/lnrpc/routerrpc/router_server_deprecated.go @@ -7,6 +7,8 @@ import ( "fmt" "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing/route" ) // legacyTrackPaymentServer is a wrapper struct that transforms a stream of main @@ -118,3 +120,44 @@ func (s *Server) SendToRoute(ctx context.Context, return legacyResp, err } + +// QueryProbability returns the current success probability estimate for a +// given node pair and amount. +func (s *Server) QueryProbability(ctx context.Context, + req *QueryProbabilityRequest) (*QueryProbabilityResponse, error) { + + fromNode, err := route.NewVertexFromBytes(req.FromNode) + if err != nil { + return nil, err + } + + toNode, err := route.NewVertexFromBytes(req.ToNode) + if err != nil { + return nil, err + } + + amt := lnwire.MilliSatoshi(req.AmtMsat) + + // Compute the probability. + var prob float64 + mc := s.cfg.RouterBackend.MissionControl + capacity, err := s.cfg.RouterBackend.FetchAmountPairCapacity( + fromNode, toNode, amt, + ) + + // If we cannot query the capacity this means that either we don't have + // information available or that the channel fails min/maxHtlc + // constraints, so we return a zero probability. + if err != nil { + log.Errorf("Cannot fetch capacity: %v", err) + } else { + prob = mc.GetProbability(fromNode, toNode, amt, capacity) + } + + history := mc.GetPairHistorySnapshot(fromNode, toNode) + + return &QueryProbabilityResponse{ + Probability: prob, + History: toRPCPairData(&history), + }, nil +} diff --git a/lntest/itest/lnd_rest_api_test.go b/lntest/itest/lnd_rest_api_test.go index c02e7da4c..16f7c6c17 100644 --- a/lntest/itest/lnd_rest_api_test.go +++ b/lntest/itest/lnd_rest_api_test.go @@ -111,7 +111,7 @@ func testRestAPI(net *lntest.NetworkHarness, ht *harnessTest) { resp := &routerrpc.QueryProbabilityResponse{} err := invokeGET(a, url, resp) require.Nil(t, err, "query probability") - assert.Greater(t, resp.Probability, 0.5, "probability") + require.Zero(t, resp.Probability) }, }, { name: "GET with map type query param", diff --git a/lntypes/comparison.go b/lntypes/comparison.go new file mode 100644 index 000000000..bffb1a55b --- /dev/null +++ b/lntypes/comparison.go @@ -0,0 +1,26 @@ +package lntypes + +import "golang.org/x/exp/constraints" + +// Number defines a type constraint for numbers. +type Number interface { + constraints.Integer | constraints.Float +} + +// Max returns the greater of the two inputs. +func Max[N Number](op1 N, op2 N) N { + if op1 > op2 { + return op1 + } + + return op2 +} + +// Min returns the lesser of the two inputs. +func Min[N Number](op1 N, op2 N) N { + if op1 < op2 { + return op1 + } + + return op2 +} diff --git a/lntypes/comparison_test.go b/lntypes/comparison_test.go new file mode 100644 index 000000000..61bf35893 --- /dev/null +++ b/lntypes/comparison_test.go @@ -0,0 +1,25 @@ +package lntypes + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +// TestMin tests getting correct minimal numbers. +func TestMin(t *testing.T) { + t.Parallel() + + require.Equal(t, 1, Min(1, 1)) + require.Equal(t, 1, Min(1, 2)) + require.Equal(t, 1.5, Min(1.5, 2.5)) +} + +// TestMax tests getting correct maximal numbers. +func TestMax(t *testing.T) { + t.Parallel() + + require.Equal(t, 1, Max(1, 1)) + require.Equal(t, 2, Max(1, 2)) + require.Equal(t, 2.5, Max(1.5, 2.5)) +} diff --git a/routing/graph.go b/routing/graph.go index 54ddd46b1..23bdd41e2 100644 --- a/routing/graph.go +++ b/routing/graph.go @@ -1,6 +1,9 @@ package routing import ( + "fmt" + + "github.com/btcsuite/btcd/btcutil" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/lnwire" @@ -20,6 +23,11 @@ type routingGraph interface { // fetchNodeFeatures returns the features of the given node. fetchNodeFeatures(nodePub route.Vertex) (*lnwire.FeatureVector, error) + + // FetchAmountPairCapacity determines the maximal capacity between two + // pairs of nodes. + FetchAmountPairCapacity(nodeFrom, nodeTo route.Vertex, + amount lnwire.MilliSatoshi) (btcutil.Amount, error) } // CachedGraph is a routingGraph implementation that retrieves from the @@ -51,9 +59,9 @@ func NewCachedGraph(sourceNode *channeldb.LightningNode, }, nil } -// close attempts to close the underlying db transaction. This is a no-op in +// Close attempts to close the underlying db transaction. This is a no-op in // case the underlying graph uses an in-memory cache. -func (g *CachedGraph) close() error { +func (g *CachedGraph) Close() error { if g.tx == nil { return nil } @@ -86,3 +94,33 @@ func (g *CachedGraph) fetchNodeFeatures(nodePub route.Vertex) ( return g.graph.FetchNodeFeatures(nodePub) } + +// FetchAmountPairCapacity determines the maximal public capacity between two +// nodes depending on the amount we try to send. +// +// NOTE: Part of the routingGraph interface. +func (g *CachedGraph) FetchAmountPairCapacity(nodeFrom, nodeTo route.Vertex, + amount lnwire.MilliSatoshi) (btcutil.Amount, error) { + + // Create unified edges for all incoming connections. + u := newNodeEdgeUnifier(g.sourceNode(), nodeTo, nil) + + err := u.addGraphPolicies(g) + if err != nil { + return 0, err + } + + edgeUnifier, ok := u.edgeUnifiers[nodeFrom] + if !ok { + return 0, fmt.Errorf("no edge info for node pair %v -> %v", + nodeFrom, nodeTo) + } + + edge := edgeUnifier.getEdgeNetwork(amount) + if edge == nil { + return 0, fmt.Errorf("no edge for node pair %v -> %v "+ + "(amount %v)", nodeFrom, nodeTo, amount) + } + + return edge.capacity, nil +} diff --git a/routing/missioncontrol.go b/routing/missioncontrol.go index 2bce890d5..86de4bc87 100644 --- a/routing/missioncontrol.go +++ b/routing/missioncontrol.go @@ -6,6 +6,7 @@ import ( "sync" "time" + "github.com/btcsuite/btcd/btcutil" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/lnwire" @@ -333,7 +334,7 @@ func (m *MissionControl) ResetHistory() error { // GetProbability is expected to return the success probability of a payment // from fromNode along edge. func (m *MissionControl) GetProbability(fromNode, toNode route.Vertex, - amt lnwire.MilliSatoshi) float64 { + amt lnwire.MilliSatoshi, capacity btcutil.Amount) float64 { m.Lock() defer m.Unlock() @@ -346,7 +347,9 @@ func (m *MissionControl) GetProbability(fromNode, toNode route.Vertex, return m.estimator.getLocalPairProbability(now, results, toNode) } - return m.estimator.getPairProbability(now, results, toNode, amt) + return m.estimator.getPairProbability( + now, results, toNode, amt, capacity, + ) } // GetHistorySnapshot takes a snapshot from the current mission control state diff --git a/routing/missioncontrol_test.go b/routing/missioncontrol_test.go index dac2cfebc..6bbe4aaf5 100644 --- a/routing/missioncontrol_test.go +++ b/routing/missioncontrol_test.go @@ -112,10 +112,13 @@ func (ctx *mcTestContext) restartMc() { func (ctx *mcTestContext) expectP(amt lnwire.MilliSatoshi, expected float64) { ctx.t.Helper() - p := ctx.mc.GetProbability(mcTestNode1, mcTestNode2, amt) - if p != expected { - ctx.t.Fatalf("expected probability %v but got %v", expected, p) - } + p := ctx.mc.GetProbability(mcTestNode1, mcTestNode2, amt, testCapacity) + + // We relax the accuracy for the probability check because of the + // capacity cutoff factor. + require.InDelta( + ctx.t, expected, p, 0.001, "probability does not match", + ) } // reportFailure reports a failure by using a test route. @@ -148,9 +151,11 @@ func TestMissionControl(t *testing.T) { testTime := time.Date(2018, time.January, 9, 14, 00, 00, 0, time.UTC) - // For local channels, we expect a higher probability than our a prior + // For local channels, we expect a higher probability than our apriori // test probability. - selfP := ctx.mc.GetProbability(mcTestSelf, mcTestNode1, 100) + selfP := ctx.mc.GetProbability( + mcTestSelf, mcTestNode1, 100, testCapacity, + ) if selfP != prevSuccessProbability { t.Fatalf("expected prev success prob for untried local chans") } diff --git a/routing/mock_graph_test.go b/routing/mock_graph_test.go index 79d46d012..dfbfd6aba 100644 --- a/routing/mock_graph_test.go +++ b/routing/mock_graph_test.go @@ -226,6 +226,31 @@ func (m *mockGraph) fetchNodeFeatures(nodePub route.Vertex) ( return lnwire.EmptyFeatureVector(), nil } +// FetchAmountPairCapacity returns the maximal capacity between nodes in the +// graph. +// +// NOTE: Part of the routingGraph interface. +func (m *mockGraph) FetchAmountPairCapacity(nodeFrom, nodeTo route.Vertex, + amount lnwire.MilliSatoshi) (btcutil.Amount, error) { + + var capacity btcutil.Amount + + cb := func(channel *channeldb.DirectedChannel) error { + if channel.OtherNode == nodeTo { + capacity = channel.Capacity + } + + return nil + } + + err := m.forEachNodeChannel(nodeFrom, cb) + if err != nil { + return 0, err + } + + return capacity, nil +} + // htlcResult describes the resolution of an htlc. If failure is nil, the htlc // was settled. type htlcResult struct { diff --git a/routing/mock_test.go b/routing/mock_test.go index afd0126dd..72f037b03 100644 --- a/routing/mock_test.go +++ b/routing/mock_test.go @@ -5,6 +5,7 @@ import ( "sync" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcutil" "github.com/go-errors/errors" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/htlcswitch" @@ -139,7 +140,7 @@ func (m *mockMissionControlOld) ReportPaymentSuccess(paymentID uint64, } func (m *mockMissionControlOld) GetProbability(fromNode, toNode route.Vertex, - amt lnwire.MilliSatoshi) float64 { + amt lnwire.MilliSatoshi, capacity btcutil.Amount) float64 { return 0 } @@ -650,9 +651,9 @@ func (m *mockMissionControl) ReportPaymentSuccess(paymentID uint64, } func (m *mockMissionControl) GetProbability(fromNode, toNode route.Vertex, - amt lnwire.MilliSatoshi) float64 { + amt lnwire.MilliSatoshi, capacity btcutil.Amount) float64 { - args := m.Called(fromNode, toNode, amt) + args := m.Called(fromNode, toNode, amt, capacity) return args.Get(0).(float64) } diff --git a/routing/pathfind.go b/routing/pathfind.go index 0d0a1e920..bffc46336 100644 --- a/routing/pathfind.go +++ b/routing/pathfind.go @@ -7,6 +7,7 @@ import ( "math" "time" + "github.com/btcsuite/btcd/btcutil" sphinx "github.com/lightningnetwork/lightning-onion" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/feature" @@ -42,7 +43,7 @@ const ( type pathFinder = func(g *graphParams, r *RestrictParams, cfg *PathFindingConfig, source, target route.Vertex, amt lnwire.MilliSatoshi, timePref float64, finalHtlcExpiry int32) ( - []*channeldb.CachedEdgePolicy, error) + []*channeldb.CachedEdgePolicy, float64, error) var ( // DefaultAttemptCost is the default fixed virtual cost in path finding @@ -306,7 +307,7 @@ type RestrictParams struct { // ProbabilitySource is a callback that is expected to return the // success probability of traversing the channel from the node. ProbabilitySource func(route.Vertex, route.Vertex, - lnwire.MilliSatoshi) float64 + lnwire.MilliSatoshi, btcutil.Amount) float64 // FeeLimit is a maximum fee amount allowed to be used on the path from // the source to the target. @@ -426,7 +427,7 @@ func getOutgoingBalance(node route.Vertex, outgoingChans map[uint64]struct{}, // available bandwidth. func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig, source, target route.Vertex, amt lnwire.MilliSatoshi, timePref float64, - finalHtlcExpiry int32) ([]*channeldb.CachedEdgePolicy, error) { + finalHtlcExpiry int32) ([]*channeldb.CachedEdgePolicy, float64, error) { // Pathfinding can be a significant portion of the total payment // latency, especially on low-powered devices. Log several metrics to @@ -447,7 +448,7 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig, var err error features, err = g.graph.fetchNodeFeatures(target) if err != nil { - return nil, err + return nil, 0, err } } @@ -456,14 +457,14 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig, err := feature.ValidateRequired(features) if err != nil { log.Warnf("Pathfinding destination node features: %v", err) - return nil, errUnknownRequiredFeature + return nil, 0, errUnknownRequiredFeature } // Ensure that all transitive dependencies are set. err = feature.ValidateDeps(features) if err != nil { log.Warnf("Pathfinding destination node features: %v", err) - return nil, errMissingDependentFeature + return nil, 0, errMissingDependentFeature } // Now that we know the feature vector is well formed, we'll proceed in @@ -475,7 +476,7 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig, if len(r.DestCustomRecords) > 0 && !features.HasFeature(lnwire.TLVOnionPayloadOptional) { - return nil, errNoTlvPayload + return nil, 0, errNoTlvPayload } // If the caller has a payment address to attach, check that our @@ -483,7 +484,7 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig, if r.PaymentAddr != nil && !features.HasFeature(lnwire.PaymentAddrOptional) { - return nil, errNoPaymentAddr + return nil, 0, errNoPaymentAddr } // If the caller needs to send custom records, check that our @@ -491,7 +492,7 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig, if r.Metadata != nil && !features.HasFeature(lnwire.TLVOnionPayloadOptional) { - return nil, errNoTlvPayload + return nil, 0, errNoTlvPayload } // Set up outgoing channel map for quicker access. @@ -512,19 +513,19 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig, self, outgoingChanMap, g.bandwidthHints, g.graph, ) if err != nil { - return nil, err + return nil, 0, err } // If the total outgoing balance isn't sufficient, it will be // impossible to complete the payment. if total < amt { - return nil, errInsufficientBalance + return nil, 0, errInsufficientBalance } // If there is only not enough capacity on a single route, it // may still be possible to complete the payment by splitting. if max < amt { - return nil, errNoPathFound + return nil, 0, errNoPathFound } } @@ -610,8 +611,8 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig, // Validate time preference value. if math.Abs(timePref) > 1 { - return nil, fmt.Errorf("time preference %v out of range [-1, 1]", - timePref) + return nil, 0, fmt.Errorf("time preference %v out of range "+ + "[-1, 1]", timePref) } // Scale to avoid the extremes -1 and 1 which run into infinity issues. @@ -628,7 +629,7 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig, // satisfy our specific requirements. processEdge := func(fromVertex route.Vertex, fromFeatures *lnwire.FeatureVector, - edge *channeldb.CachedEdgePolicy, toNodeDist *nodeWithDist) { + edge *unifiedEdge, toNodeDist *nodeWithDist) { edgesExpanded++ @@ -639,6 +640,7 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig, // Request the success probability for this edge. edgeProbability := r.ProbabilitySource( fromVertex, toNodeDist.node, amountToSend, + edge.capacity, ) log.Trace(newLogClosure(func() string { @@ -666,8 +668,8 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig, var fee lnwire.MilliSatoshi var timeLockDelta uint16 if fromVertex != source { - fee = edge.ComputeFee(amountToSend) - timeLockDelta = edge.TimeLockDelta + fee = edge.policy.ComputeFee(amountToSend) + timeLockDelta = edge.policy.TimeLockDelta } incomingCltv := toNodeDist.incomingCltv + int32(timeLockDelta) @@ -744,9 +746,9 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig, // Every edge should have a positive time lock delta. If we // encounter a zero delta, log a warning line. - if edge.TimeLockDelta == 0 { + if edge.policy.TimeLockDelta == 0 { log.Warnf("Channel %v has zero cltv delta", - edge.ChannelID) + edge.policy.ChannelID) } // Calculate the total routing info size if this hop were to be @@ -767,7 +769,7 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig, LegacyPayload: !supportsTlv, } - payloadSize = hop.PayloadSize(edge.ChannelID) + payloadSize = hop.PayloadSize(edge.policy.ChannelID) } routingInfoSize := toNodeDist.routingInfoSize + payloadSize @@ -788,7 +790,7 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig, amountToReceive: amountToReceive, incomingCltv: incomingCltv, probability: probability, - nextHop: edge, + nextHop: edge.policy, routingInfoSize: routingInfoSize, } distance[fromVertex] = withDist @@ -849,12 +851,12 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig, pivot := partialPath.node - // Create unified policies for all incoming connections. - u := newUnifiedPolicies(self, pivot, outgoingChanMap) + // Create unified edges for all incoming connections. + u := newNodeEdgeUnifier(self, pivot, outgoingChanMap) err := u.addGraphPolicies(g.graph) if err != nil { - return nil, err + return nil, 0, err } for _, reverseEdge := range additionalEdgesWithSrc[pivot] { @@ -865,7 +867,7 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig, // Expand all connections using the optimal policy for each // connection. - for fromNode, unifiedPolicy := range u.policies { + for fromNode, edgeUnifier := range u.edgeUnifiers { // The target node is not recorded in the distance map. // Therefore we need to have this check to prevent // creating a cycle. Only when we intend to route to @@ -882,18 +884,18 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig, continue } - policy := unifiedPolicy.getPolicy( + edge := edgeUnifier.getEdge( amtToSend, g.bandwidthHints, ) - if policy == nil { + if edge == nil { continue } // Get feature vector for fromNode. fromFeatures, err := getGraphFeatures(fromNode) if err != nil { - return nil, err + return nil, 0, err } // If there are no valid features, skip this node. @@ -903,7 +905,7 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig, // Check if this candidate node is better than what we // already have. - processEdge(fromNode, fromFeatures, policy, partialPath) + processEdge(fromNode, fromFeatures, edge, partialPath) } if nodeHeap.Len() == 0 { @@ -932,7 +934,7 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig, if !ok { // If the node doesn't have a next hop it means we // didn't find a path. - return nil, errNoPathFound + return nil, 0, errNoPathFound } // Add the next hop to the list of path edges. @@ -966,7 +968,7 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig, distance[source].probability, len(pathEdges), distance[source].amountToReceive-amt) - return pathEdges, nil + return pathEdges, distance[source].probability, nil } // getProbabilityBasedDist converts a weight into a distance that takes into diff --git a/routing/pathfind_test.go b/routing/pathfind_test.go index a3202d1e9..3fb0dc9ce 100644 --- a/routing/pathfind_test.go +++ b/routing/pathfind_test.go @@ -105,7 +105,9 @@ var ( // noProbabilitySource is used in testing to return the same probability 1 for // all edges. -func noProbabilitySource(route.Vertex, route.Vertex, lnwire.MilliSatoshi) float64 { +func noProbabilitySource(route.Vertex, route.Vertex, lnwire.MilliSatoshi, + btcutil.Amount) float64 { + return 1 } @@ -2289,7 +2291,7 @@ func TestPathFindSpecExample(t *testing.T) { // Query for a route of 4,999,999 mSAT to carol. carol := ctx.aliases["C"] const amt lnwire.MilliSatoshi = 4999999 - route, err := ctx.router.FindRoute( + route, _, err := ctx.router.FindRoute( bobNode.PubKeyBytes, carol, amt, 0, noRestrictions, nil, nil, MinCLTVDelta, ) @@ -2339,7 +2341,7 @@ func TestPathFindSpecExample(t *testing.T) { } // We'll now request a route from A -> B -> C. - route, err = ctx.router.FindRoute( + route, _, err = ctx.router.FindRoute( source.PubKeyBytes, carol, amt, 0, noRestrictions, nil, nil, MinCLTVDelta, ) @@ -2796,8 +2798,9 @@ func testProbabilityRouting(t *testing.T, useCache bool, target := ctx.testGraphInstance.aliasMap["target"] // Configure a probability source with the test parameters. - ctx.restrictParams.ProbabilitySource = func(fromNode, toNode route.Vertex, - amt lnwire.MilliSatoshi) float64 { + ctx.restrictParams.ProbabilitySource = func(fromNode, + toNode route.Vertex, amt lnwire.MilliSatoshi, + capacity btcutil.Amount) float64 { if amt == 0 { t.Fatal("expected non-zero amount") @@ -2878,8 +2881,9 @@ func runEqualCostRouteSelection(t *testing.T, useCache bool) { paymentAmt := lnwire.NewMSatFromSatoshis(100) target := ctx.testGraphInstance.aliasMap["target"] - ctx.restrictParams.ProbabilitySource = func(fromNode, toNode route.Vertex, - amt lnwire.MilliSatoshi) float64 { + ctx.restrictParams.ProbabilitySource = func(fromNode, + toNode route.Vertex, amt lnwire.MilliSatoshi, + capacity btcutil.Amount) float64 { switch { case fromNode == alias["source"] && toNode == alias["a"]: @@ -3108,12 +3112,12 @@ func dbFindPath(graph *channeldb.ChannelGraph, } defer func() { - if err := routingGraph.close(); err != nil { + if err := routingGraph.Close(); err != nil { log.Errorf("Error closing db tx: %v", err) } }() - return findPath( + route, _, err := findPath( &graphParams{ additionalEdges: additionalEdges, bandwidthHints: bandwidthHints, @@ -3121,4 +3125,6 @@ func dbFindPath(graph *channeldb.ChannelGraph, }, r, cfg, source, target, amt, timePref, finalHtlcExpiry, ) + + return route, err } diff --git a/routing/payment_session.go b/routing/payment_session.go index f3092ee7b..8b181a04d 100644 --- a/routing/payment_session.go +++ b/routing/payment_session.go @@ -297,7 +297,7 @@ func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi, sourceVertex := routingGraph.sourceNode() // Find a route for the current amount. - path, err := p.pathFinder( + path, _, err := p.pathFinder( &graphParams{ additionalEdges: p.additionalEdges, bandwidthHints: bandwidthHints, diff --git a/routing/payment_session_source.go b/routing/payment_session_source.go index 1a589fd4e..9d42f6a4a 100644 --- a/routing/payment_session_source.go +++ b/routing/payment_session_source.go @@ -51,7 +51,7 @@ func (m *SessionSource) getRoutingGraph() (routingGraph, func(), error) { return nil, nil, err } return routingTx, func() { - err := routingTx.close() + err := routingTx.Close() if err != nil { log.Errorf("Error closing db tx: %v", err) } diff --git a/routing/payment_session_test.go b/routing/payment_session_test.go index 4eaa5af66..18858f1f9 100644 --- a/routing/payment_session_test.go +++ b/routing/payment_session_test.go @@ -209,11 +209,10 @@ func TestRequestRoute(t *testing.T) { } // Override pathfinder with a mock. - session.pathFinder = func( - g *graphParams, r *RestrictParams, cfg *PathFindingConfig, - source, target route.Vertex, amt lnwire.MilliSatoshi, - timePref float64, - finalHtlcExpiry int32) ([]*channeldb.CachedEdgePolicy, error) { + session.pathFinder = func(_ *graphParams, r *RestrictParams, + _ *PathFindingConfig, _, _ route.Vertex, _ lnwire.MilliSatoshi, + _ float64, _ int32) ([]*channeldb.CachedEdgePolicy, float64, + error) { // We expect find path to receive a cltv limit excluding the // final cltv delta (including the block padding). @@ -232,7 +231,7 @@ func TestRequestRoute(t *testing.T) { }, } - return path, nil + return path, 1.0, nil } route, err := session.RequestRoute( diff --git a/routing/probability_estimator.go b/routing/probability_estimator.go index 851cecf16..320c16bda 100644 --- a/routing/probability_estimator.go +++ b/routing/probability_estimator.go @@ -5,10 +5,48 @@ import ( "math" "time" + "github.com/btcsuite/btcd/btcutil" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/routing/route" ) +const ( + // capacityCutoffFraction and capacitySmearingFraction define how + // capacity-related probability reweighting works. + // capacityCutoffFraction defines the fraction of the channel capacity + // at which the effect roughly sets in and capacitySmearingFraction + // defines over which range the factor changes from 1 to 0. + // + // We may fall below the minimum required probability + // (DefaultMinRouteProbability) when the amount comes close to the + // available capacity of a single channel of the route in case of no + // prior knowledge about the channels. We want such routes still to be + // available and therefore a probability reduction should not completely + // drop the total probability below DefaultMinRouteProbability. + // For this to hold for a three-hop route we require: + // (DefaultAprioriHopProbability)^3 * minCapacityFactor > + // DefaultMinRouteProbability + // + // For DefaultAprioriHopProbability = 0.6 and + // DefaultMinRouteProbability = 0.01 this results in + // minCapacityFactor ~ 0.05. The following combination of parameters + // fulfill the requirement with capacityFactor(cap, cap) ~ 0.076 (see + // tests). + + // The capacityCutoffFraction is a trade-off between usage of the + // provided capacity and expected probability reduction when we send the + // full amount. The success probability in the random balance model can + // be approximated with P(a) = 1 - a/c, for amount a and capacity c. If + // we require a probability P(a) > 0.25, this translates into a value of + // 0.75 for a/c. + capacityCutoffFraction = 0.75 + + // We don't want to have a sharp drop of the capacity factor to zero at + // capacityCutoffFraction, but a smooth smearing such that some residual + // probability is left when spending the whole amount, see above. + capacitySmearingFraction = 0.1 +) + var ( // ErrInvalidHalflife is returned when we get an invalid half life. ErrInvalidHalflife = errors.New("penalty half life must be >= 0") @@ -74,18 +112,23 @@ type probabilityEstimator struct { // that have not been tried before. The results parameter is a list of last // payment results for that node. func (p *probabilityEstimator) getNodeProbability(now time.Time, - results NodeResults, amt lnwire.MilliSatoshi) float64 { + results NodeResults, amt lnwire.MilliSatoshi, + capacity btcutil.Amount) float64 { + + // We reduce the apriori hop probability if the amount comes close to + // the capacity. + apriori := p.AprioriHopProbability * capacityFactor(amt, capacity) // If the channel history is not to be taken into account, we can return // early here with the configured a priori probability. if p.AprioriWeight == 1 { - return p.AprioriHopProbability + return apriori } // If there is no channel history, our best estimate is still the a // priori probability. if len(results) == 0 { - return p.AprioriHopProbability + return apriori } // The value of the apriori weight is in the range [0, 1]. Convert it to @@ -113,7 +156,7 @@ func (p *probabilityEstimator) getNodeProbability(now time.Time, // effectively prunes all channels of the node forever. This is the most // aggressive way in which we can penalize nodes and unlikely to yield // good results in a real network. - probabilitiesTotal := p.AprioriHopProbability * aprioriFactor + probabilitiesTotal := apriori * aprioriFactor totalWeight := aprioriFactor for _, result := range results { @@ -146,14 +189,44 @@ func (p *probabilityEstimator) getWeight(age time.Duration) float64 { return math.Pow(2, exp) } +// capacityFactor is a multiplier that can be used to reduce the probability +// depending on how much of the capacity is sent. The limits are 1 for amt == 0 +// and 0 for amt >> cutoffMsat. The function drops significantly when amt +// reaches cutoffMsat. smearingMsat determines over which scale the reduction +// takes place. +func capacityFactor(amt lnwire.MilliSatoshi, capacity btcutil.Amount) float64 { + // If we don't have information about the capacity, which can be the + // case for hop hints or local channels, we return unity to not alter + // anything. + if capacity == 0 { + return 1.0 + } + + capMsat := float64(lnwire.NewMSatFromSatoshis(capacity)) + amtMsat := float64(amt) + + if amtMsat > capMsat { + return 0 + } + + cutoffMsat := capacityCutoffFraction * capMsat + smearingMsat := capacitySmearingFraction * capMsat + + // We compute a logistic function mirrored around the y axis, centered + // at cutoffMsat, decaying over the smearingMsat scale. + denominator := 1 + math.Exp(-(amtMsat-cutoffMsat)/smearingMsat) + + return 1 - 1/denominator +} + // getPairProbability estimates the probability of successfully traversing to // toNode based on historical payment outcomes for the from node. Those outcomes // are passed in via the results parameter. func (p *probabilityEstimator) getPairProbability( - now time.Time, results NodeResults, - toNode route.Vertex, amt lnwire.MilliSatoshi) float64 { + now time.Time, results NodeResults, toNode route.Vertex, + amt lnwire.MilliSatoshi, capacity btcutil.Amount) float64 { - nodeProbability := p.getNodeProbability(now, results, amt) + nodeProbability := p.getNodeProbability(now, results, amt, capacity) return p.calculateProbability( now, results, nodeProbability, toNode, amt, diff --git a/routing/probability_estimator_test.go b/routing/probability_estimator_test.go index 3bfb0536a..2e683fc2d 100644 --- a/routing/probability_estimator_test.go +++ b/routing/probability_estimator_test.go @@ -4,8 +4,10 @@ import ( "testing" "time" + "github.com/btcsuite/btcd/btcutil" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/routing/route" + "github.com/stretchr/testify/require" ) const ( @@ -23,6 +25,13 @@ const ( aprioriHopProb = 0.6 aprioriWeight = 0.75 aprioriPrevSucProb = 0.95 + + // testCapacity is used to define a capacity for some channels. + testCapacity = btcutil.Amount(100_000) + testAmount = lnwire.MilliSatoshi(50_000_000) + + // Defines the capacityFactor for testAmount and testCapacity. + capFactor = 0.9241 ) type estimatorTestContext struct { @@ -53,7 +62,8 @@ func newEstimatorTestContext(t *testing.T) *estimatorTestContext { // assertPairProbability asserts that the calculated success probability is // correct. func (c *estimatorTestContext) assertPairProbability(now time.Time, - toNode byte, amt lnwire.MilliSatoshi, expectedProb float64) { + toNode byte, amt lnwire.MilliSatoshi, capacity btcutil.Amount, + expectedProb float64) { c.t.Helper() @@ -64,7 +74,9 @@ func (c *estimatorTestContext) assertPairProbability(now time.Time, const tolerance = 0.01 - p := c.estimator.getPairProbability(now, results, route.Vertex{toNode}, amt) + p := c.estimator.getPairProbability( + now, results, route.Vertex{toNode}, amt, capacity, + ) diff := p - expectedProb if diff > tolerance || diff < -tolerance { c.t.Fatalf("expected probability %v for node %v, but got %v", @@ -77,7 +89,16 @@ func (c *estimatorTestContext) assertPairProbability(now time.Time, func TestProbabilityEstimatorNoResults(t *testing.T) { ctx := newEstimatorTestContext(t) - ctx.assertPairProbability(testTime, 0, 0, aprioriHopProb) + // A zero amount does not trigger capacity rescaling. + ctx.assertPairProbability( + testTime, 0, 0, testCapacity, aprioriHopProb, + ) + + // We expect a reduced probability when a higher amount is used. + expected := aprioriHopProb * capFactor + ctx.assertPairProbability( + testTime, 0, testAmount, testCapacity, expected, + ) } // TestProbabilityEstimatorOneSuccess tests the probability estimation for nodes @@ -87,20 +108,37 @@ func TestProbabilityEstimatorOneSuccess(t *testing.T) { ctx.results = map[int]TimedPairResult{ node1: { - SuccessAmt: lnwire.MilliSatoshi(1000), + SuccessAmt: testAmount, }, } // Because of the previous success, this channel keep reporting a high // probability. ctx.assertPairProbability( - testTime, node1, 100, aprioriPrevSucProb, + testTime, node1, 100, testCapacity, aprioriPrevSucProb, + ) + + // The apriori success probability indicates that in the past we were + // able to send the full amount. We don't want to reduce this + // probability with the capacity factor, which is tested here. + ctx.assertPairProbability( + testTime, node1, testAmount, testCapacity, aprioriPrevSucProb, ) // Untried channels are also influenced by the success. With a // aprioriWeight of 0.75, the a priori probability is assigned weight 3. expectedP := (3*aprioriHopProb + 1*aprioriPrevSucProb) / 4 - ctx.assertPairProbability(testTime, untriedNode, 100, expectedP) + ctx.assertPairProbability( + testTime, untriedNode, 100, testCapacity, expectedP, + ) + + // Check that the correct probability is computed for larger amounts. + apriori := aprioriHopProb * capFactor + + expectedP = (3*apriori + 1*aprioriPrevSucProb) / 4 + ctx.assertPairProbability( + testTime, untriedNode, testAmount, testCapacity, expectedP, + ) } // TestProbabilityEstimatorOneFailure tests the probability estimation for nodes @@ -119,11 +157,15 @@ func TestProbabilityEstimatorOneFailure(t *testing.T) { // the failure after one hour is 0.5. This makes the node probability // 0.51: expectedNodeProb := (3*aprioriHopProb + 0.5*0) / 3.5 - ctx.assertPairProbability(testTime, untriedNode, 100, expectedNodeProb) + ctx.assertPairProbability( + testTime, untriedNode, 100, testCapacity, expectedNodeProb, + ) // The pair probability decays back to the node probability. With the // weight at 0.5, we expected a pair probability of 0.5 * 0.51 = 0.25. - ctx.assertPairProbability(testTime, node1, 100, expectedNodeProb/2) + ctx.assertPairProbability( + testTime, node1, 100, testCapacity, expectedNodeProb/2, + ) } // TestProbabilityEstimatorMix tests the probability estimation for nodes for @@ -147,7 +189,9 @@ func TestProbabilityEstimatorMix(t *testing.T) { // We expect the probability for a previously successful channel to // remain high. - ctx.assertPairProbability(testTime, node1, 100, prevSuccessProbability) + ctx.assertPairProbability( + testTime, node1, 100, testCapacity, prevSuccessProbability, + ) // For an untried node, we expected the node probability to be returned. // This is a weighted average of the results above and the a priori @@ -155,9 +199,83 @@ func TestProbabilityEstimatorMix(t *testing.T) { expectedNodeProb := (3*aprioriHopProb + 1*prevSuccessProbability) / (3 + 1 + 0.25 + 0.125) - ctx.assertPairProbability(testTime, untriedNode, 100, expectedNodeProb) + ctx.assertPairProbability( + testTime, untriedNode, 100, testCapacity, expectedNodeProb, + ) // For the previously failed connection with node 1, we expect 0.75 * // the node probability = 0.47. - ctx.assertPairProbability(testTime, node2, 100, expectedNodeProb*0.75) + ctx.assertPairProbability( + testTime, node2, 100, testCapacity, expectedNodeProb*0.75, + ) +} + +// TestCapacityCutoff tests the mathematical expression and limits for the +// capacity factor. +func TestCapacityCutoff(t *testing.T) { + t.Parallel() + + capacitySat := 1_000_000 + capacityMSat := capacitySat * 1000 + + tests := []struct { + name string + amountMsat int + expectedFactor float64 + }{ + { + name: "zero amount", + expectedFactor: 1, + }, + { + name: "low amount", + amountMsat: capacityMSat / 10, + expectedFactor: 0.998, + }, + { + name: "half amount", + amountMsat: capacityMSat / 2, + expectedFactor: 0.924, + }, + { + name: "cutoff amount", + amountMsat: int( + capacityCutoffFraction * float64(capacityMSat), + ), + expectedFactor: 0.5, + }, + { + name: "high amount", + amountMsat: capacityMSat * 80 / 100, + expectedFactor: 0.377, + }, + { + // Even when we spend the full capacity, we still want + // to have some residual probability to not throw away + // routes due to a min probability requirement of the + // whole path. + name: "full amount", + amountMsat: capacityMSat, + expectedFactor: 0.076, + }, + { + name: "more than capacity", + amountMsat: capacityMSat + 1, + expectedFactor: 0.0, + }, + } + + for _, test := range tests { + test := test + + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + got := capacityFactor( + lnwire.MilliSatoshi(test.amountMsat), + btcutil.Amount(capacitySat), + ) + require.InDelta(t, test.expectedFactor, got, 0.001) + }) + } } diff --git a/routing/router.go b/routing/router.go index 0588d5122..378f45187 100644 --- a/routing/router.go +++ b/routing/router.go @@ -227,7 +227,7 @@ type MissionController interface { // GetProbability is expected to return the success probability of a // payment from fromNode along edge. GetProbability(fromNode, toNode route.Vertex, - amt lnwire.MilliSatoshi) float64 + amt lnwire.MilliSatoshi, capacity btcutil.Amount) float64 } // FeeSchema is the set fee configuration for a Lightning Node on the network. @@ -1752,11 +1752,10 @@ type routingMsg struct { // particular target destination to which it is able to send `amt` after // factoring in channel capacities and cumulative fees along the route. func (r *ChannelRouter) FindRoute(source, target route.Vertex, - amt lnwire.MilliSatoshi, timePref float64, - restrictions *RestrictParams, + amt lnwire.MilliSatoshi, timePref float64, restrictions *RestrictParams, destCustomRecords record.CustomSet, routeHints map[route.Vertex][]*channeldb.CachedEdgePolicy, - finalExpiry uint16) (*route.Route, error) { + finalExpiry uint16) (*route.Route, float64, error) { log.Debugf("Searching for path to %v, sending %v", target, amt) @@ -1766,14 +1765,14 @@ func (r *ChannelRouter) FindRoute(source, target route.Vertex, r.cachedGraph, r.selfNode.PubKeyBytes, r.cfg.GetLink, ) if err != nil { - return nil, err + return nil, 0, err } // We'll fetch the current block height so we can properly calculate the // required HTLC time locks within the route. _, currentHeight, err := r.cfg.Chain.GetBestBlock() if err != nil { - return nil, err + return nil, 0, err } // Now that we know the destination is reachable within the graph, we'll @@ -1782,10 +1781,10 @@ func (r *ChannelRouter) FindRoute(source, target route.Vertex, // Validate time preference. if timePref < -1 || timePref > 1 { - return nil, errors.New("time preference out of range") + return nil, 0, errors.New("time preference out of range") } - path, err := findPath( + path, probability, err := findPath( &graphParams{ additionalEdges: routeHints, bandwidthHints: bandwidthHints, @@ -1796,7 +1795,7 @@ func (r *ChannelRouter) FindRoute(source, target route.Vertex, source, target, amt, timePref, finalHtlcExpiry, ) if err != nil { - return nil, err + return nil, 0, err } // Create the route with absolute time lock values. @@ -1810,7 +1809,7 @@ func (r *ChannelRouter) FindRoute(source, target route.Vertex, }, ) if err != nil { - return nil, err + return nil, 0, err } go log.Tracef("Obtained path to send %v to %x: %v", @@ -1819,7 +1818,7 @@ func (r *ChannelRouter) FindRoute(source, target route.Vertex, }), ) - return route, nil + return route, probability, nil } // generateNewSessionKey generates a new ephemeral private key to be used for a @@ -2765,9 +2764,8 @@ func (r *ChannelRouter) BuildRoute(amt *lnwire.MilliSatoshi, return nil, err } - // Allocate a list that will contain the unified policies for this - // route. - edges := make([]*unifiedPolicy, len(hops)) + // Allocate a list that will contain the edge unifiers for this route. + unifiers := make([]*edgeUnifier, len(hops)) var runningAmt lnwire.MilliSatoshi if useMinAmt { @@ -2796,9 +2794,9 @@ func (r *ChannelRouter) BuildRoute(amt *lnwire.MilliSatoshi, localChan := i == 0 - // Build unified policies for this hop based on the channels - // known in the graph. - u := newUnifiedPolicies(source, toNode, outgoingChans) + // Build unified edges for this hop based on the channels known + // in the graph. + u := newNodeEdgeUnifier(source, toNode, outgoingChans) err := u.addGraphPolicies(r.cachedGraph) if err != nil { @@ -2806,7 +2804,7 @@ func (r *ChannelRouter) BuildRoute(amt *lnwire.MilliSatoshi, } // Exit if there are no channels. - unifiedPolicy, ok := u.policies[fromNode] + edgeUnifier, ok := u.edgeUnifiers[fromNode] if !ok { log.Errorf("Cannot find policy for node %v", fromNode) return nil, ErrNoChannel{ @@ -2817,18 +2815,18 @@ func (r *ChannelRouter) BuildRoute(amt *lnwire.MilliSatoshi, // If using min amt, increase amt if needed. if useMinAmt { - min := unifiedPolicy.minAmt() + min := edgeUnifier.minAmt() if min > runningAmt { runningAmt = min } } - // Get a forwarding policy for the specific amount that we want - // to forward. - policy := unifiedPolicy.getPolicy(runningAmt, bandwidthHints) - if policy == nil { + // Get an edge for the specific amount that we want to forward. + edge := edgeUnifier.getEdge(runningAmt, bandwidthHints) + if edge == nil { log.Errorf("Cannot find policy with amt=%v for node %v", runningAmt, fromNode) + return nil, ErrNoChannel{ fromNode: fromNode, position: i, @@ -2837,12 +2835,13 @@ func (r *ChannelRouter) BuildRoute(amt *lnwire.MilliSatoshi, // Add fee for this hop. if !localChan { - runningAmt += policy.ComputeFee(runningAmt) + runningAmt += edge.policy.ComputeFee(runningAmt) } - log.Tracef("Select channel %v at position %v", policy.ChannelID, i) + log.Tracef("Select channel %v at position %v", + edge.policy.ChannelID, i) - edges[i] = unifiedPolicy + unifiers[i] = edgeUnifier } // Now that we arrived at the start of the route and found out the route @@ -2851,9 +2850,9 @@ func (r *ChannelRouter) BuildRoute(amt *lnwire.MilliSatoshi, // amount ranges re-checked. var pathEdges []*channeldb.CachedEdgePolicy receiverAmt := runningAmt - for i, edge := range edges { - policy := edge.getPolicy(receiverAmt, bandwidthHints) - if policy == nil { + for i, unifier := range unifiers { + edge := unifier.getEdge(receiverAmt, bandwidthHints) + if edge == nil { return nil, ErrNoChannel{ fromNode: hops[i-1], position: i, @@ -2862,12 +2861,12 @@ func (r *ChannelRouter) BuildRoute(amt *lnwire.MilliSatoshi, if i > 0 { // Decrease the amount to send while going forward. - receiverAmt -= policy.ComputeFeeFromIncoming( + receiverAmt -= edge.policy.ComputeFeeFromIncoming( receiverAmt, ) } - pathEdges = append(pathEdges, policy) + pathEdges = append(pathEdges, edge.policy) } // Build and return the final route. diff --git a/routing/router_test.go b/routing/router_test.go index ea76140a2..0346721e3 100644 --- a/routing/router_test.go +++ b/routing/router_test.go @@ -268,7 +268,7 @@ func TestFindRoutesWithFeeLimit(t *testing.T) { CltvLimit: math.MaxUint32, } - route, err := ctx.router.FindRoute( + route, _, err := ctx.router.FindRoute( ctx.router.selfNode.PubKeyBytes, target, paymentAmt, 0, restrictions, nil, nil, MinCLTVDelta, @@ -1554,7 +1554,7 @@ func TestAddEdgeUnknownVertexes(t *testing.T) { targetNode := priv2.PubKey() var targetPubKeyBytes route.Vertex copy(targetPubKeyBytes[:], targetNode.SerializeCompressed()) - _, err = ctx.router.FindRoute( + _, _, err = ctx.router.FindRoute( ctx.router.selfNode.PubKeyBytes, targetPubKeyBytes, paymentAmt, 0, noRestrictions, nil, nil, MinCLTVDelta, @@ -1595,7 +1595,7 @@ func TestAddEdgeUnknownVertexes(t *testing.T) { // Should still be able to find the route, and the info should be // updated. - _, err = ctx.router.FindRoute( + _, _, err = ctx.router.FindRoute( ctx.router.selfNode.PubKeyBytes, targetPubKeyBytes, paymentAmt, 0, noRestrictions, nil, nil, MinCLTVDelta, diff --git a/routing/unified_policies.go b/routing/unified_edges.go similarity index 63% rename from routing/unified_policies.go rename to routing/unified_edges.go index ec7e594d5..a96a5ffbe 100644 --- a/routing/unified_policies.go +++ b/routing/unified_edges.go @@ -3,20 +3,21 @@ package routing import ( "github.com/btcsuite/btcd/btcutil" "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/routing/route" ) -// unifiedPolicies holds all unified policies for connections towards a node. -type unifiedPolicies struct { - // policies contains a unified policy for every from node. - policies map[route.Vertex]*unifiedPolicy +// nodeEdgeUnifier holds all edge unifiers for connections towards a node. +type nodeEdgeUnifier struct { + // edgeUnifiers contains an edge unifier for every from node. + edgeUnifiers map[route.Vertex]*edgeUnifier // sourceNode is the sender of a payment. The rules to pick the final // policy are different for local channels. sourceNode route.Vertex - // toNode is the node for which the unified policies are instantiated. + // toNode is the node for which the edge unifiers are instantiated. toNode route.Vertex // outChanRestr is an optional outgoing channel restriction for the @@ -24,13 +25,13 @@ type unifiedPolicies struct { outChanRestr map[uint64]struct{} } -// newUnifiedPolicies instantiates a new unifiedPolicies object. Channel +// newNodeEdgeUnifier instantiates a new nodeEdgeUnifier object. Channel // policies can be added to this object. -func newUnifiedPolicies(sourceNode, toNode route.Vertex, - outChanRestr map[uint64]struct{}) *unifiedPolicies { +func newNodeEdgeUnifier(sourceNode, toNode route.Vertex, + outChanRestr map[uint64]struct{}) *nodeEdgeUnifier { - return &unifiedPolicies{ - policies: make(map[route.Vertex]*unifiedPolicy), + return &nodeEdgeUnifier{ + edgeUnifiers: make(map[route.Vertex]*edgeUnifier), toNode: toNode, sourceNode: sourceNode, outChanRestr: outChanRestr, @@ -39,7 +40,7 @@ func newUnifiedPolicies(sourceNode, toNode route.Vertex, // addPolicy adds a single channel policy. Capacity may be zero if unknown // (light clients). -func (u *unifiedPolicies) addPolicy(fromNode route.Vertex, +func (u *nodeEdgeUnifier) addPolicy(fromNode route.Vertex, edge *channeldb.CachedEdgePolicy, capacity btcutil.Amount) { localChan := fromNode == u.sourceNode @@ -51,16 +52,16 @@ func (u *unifiedPolicies) addPolicy(fromNode route.Vertex, } } - // Update the policies map. - policy, ok := u.policies[fromNode] + // Update the edgeUnifiers map. + unifier, ok := u.edgeUnifiers[fromNode] if !ok { - policy = &unifiedPolicy{ + unifier = &edgeUnifier{ localChan: localChan, } - u.policies[fromNode] = policy + u.edgeUnifiers[fromNode] = unifier } - policy.edges = append(policy.edges, &unifiedPolicyEdge{ + unifier.edges = append(unifier.edges, &unifiedEdge{ policy: edge, capacity: capacity, }) @@ -68,7 +69,7 @@ func (u *unifiedPolicies) addPolicy(fromNode route.Vertex, // addGraphPolicies adds all policies that are known for the toNode in the // graph. -func (u *unifiedPolicies) addGraphPolicies(g routingGraph) error { +func (u *nodeEdgeUnifier) addGraphPolicies(g routingGraph) error { cb := func(channel *channeldb.DirectedChannel) error { // If there is no edge policy for this candidate node, skip. // Note that we are searching backwards so this node would have @@ -77,7 +78,7 @@ func (u *unifiedPolicies) addGraphPolicies(g routingGraph) error { return nil } - // Add this policy to the unified policies map. + // Add this policy to the corresponding edgeUnifier. u.addPolicy( channel.OtherNode, channel.InPolicy, channel.Capacity, ) @@ -89,16 +90,16 @@ func (u *unifiedPolicies) addGraphPolicies(g routingGraph) error { return g.forEachNodeChannel(u.toNode, cb) } -// unifiedPolicyEdge is the individual channel data that is kept inside an -// unifiedPolicy object. -type unifiedPolicyEdge struct { +// unifiedEdge is the individual channel data that is kept inside an edgeUnifier +// object. +type unifiedEdge struct { policy *channeldb.CachedEdgePolicy capacity btcutil.Amount } // amtInRange checks whether an amount falls within the valid range for a // channel. -func (u *unifiedPolicyEdge) amtInRange(amt lnwire.MilliSatoshi) bool { +func (u *unifiedEdge) amtInRange(amt lnwire.MilliSatoshi) bool { // If the capacity is available (non-light clients), skip channels that // are too small. if u.capacity > 0 && @@ -122,33 +123,32 @@ func (u *unifiedPolicyEdge) amtInRange(amt lnwire.MilliSatoshi) bool { return true } -// unifiedPolicy is the unified policy that covers all channels between a pair -// of nodes. -type unifiedPolicy struct { - edges []*unifiedPolicyEdge +// edgeUnifier is an object that covers all channels between a pair of nodes. +type edgeUnifier struct { + edges []*unifiedEdge localChan bool } -// getPolicy returns the optimal policy to use for this connection given a +// getEdge returns the optimal unified edge to use for this connection given a // specific amount to send. It differentiates between local and network // channels. -func (u *unifiedPolicy) getPolicy(amt lnwire.MilliSatoshi, - bandwidthHints bandwidthHints) *channeldb.CachedEdgePolicy { +func (u *edgeUnifier) getEdge(amt lnwire.MilliSatoshi, + bandwidthHints bandwidthHints) *unifiedEdge { if u.localChan { - return u.getPolicyLocal(amt, bandwidthHints) + return u.getEdgeLocal(amt, bandwidthHints) } - return u.getPolicyNetwork(amt) + return u.getEdgeNetwork(amt) } -// getPolicyLocal returns the optimal policy to use for this local connection -// given a specific amount to send. -func (u *unifiedPolicy) getPolicyLocal(amt lnwire.MilliSatoshi, - bandwidthHints bandwidthHints) *channeldb.CachedEdgePolicy { +// getEdgeLocal returns the optimal unified edge to use for this local +// connection given a specific amount to send. +func (u *edgeUnifier) getEdgeLocal(amt lnwire.MilliSatoshi, + bandwidthHints bandwidthHints) *unifiedEdge { var ( - bestPolicy *channeldb.CachedEdgePolicy + bestEdge *unifiedEdge maxBandwidth lnwire.MilliSatoshi ) @@ -191,23 +191,23 @@ func (u *unifiedPolicy) getPolicyLocal(amt lnwire.MilliSatoshi, } maxBandwidth = bandwidth - // Update best policy. - bestPolicy = edge.policy + // Update best edge. + bestEdge = &unifiedEdge{policy: edge.policy} } - return bestPolicy + return bestEdge } -// getPolicyNetwork returns the optimal policy to use for this connection given -// a specific amount to send. The goal is to return a policy that maximizes the -// probability of a successful forward in a non-strict forwarding context. -func (u *unifiedPolicy) getPolicyNetwork( - amt lnwire.MilliSatoshi) *channeldb.CachedEdgePolicy { - +// getEdgeNetwork returns the optimal unified edge to use for this connection +// given a specific amount to send. The goal is to return a unified edge with a +// policy that maximizes the probability of a successful forward in a non-strict +// forwarding context. +func (u *edgeUnifier) getEdgeNetwork(amt lnwire.MilliSatoshi) *unifiedEdge { var ( bestPolicy *channeldb.CachedEdgePolicy maxFee lnwire.MilliSatoshi maxTimelock uint16 + maxCapMsat lnwire.MilliSatoshi ) for _, edge := range u.edges { @@ -223,11 +223,23 @@ func (u *unifiedPolicy) getPolicyNetwork( continue } + // Track the maximal capacity for usable channels. If we don't + // know the capacity, we fall back to MaxHTLC. + capMsat := lnwire.NewMSatFromSatoshis(edge.capacity) + if capMsat == 0 && edge.policy.MessageFlags.HasMaxHtlc() { + log.Tracef("No capacity available for channel %v, "+ + "using MaxHtlcMsat (%v) as a fallback.", + edge.policy.ChannelID, edge.policy.MaxHTLC) + + capMsat = edge.policy.MaxHTLC + } + maxCapMsat = lntypes.Max(capMsat, maxCapMsat) + // Track the maximum time lock of all channels that are // candidate for non-strict forwarding at the routing node. - if edge.policy.TimeLockDelta > maxTimelock { - maxTimelock = edge.policy.TimeLockDelta - } + maxTimelock = lntypes.Max( + maxTimelock, edge.policy.TimeLockDelta, + ) // Use the policy that results in the highest fee for this // specific amount. @@ -255,19 +267,19 @@ func (u *unifiedPolicy) getPolicyNetwork( // get forwarded. Because we penalize pair-wise, there won't be a second // chance for this node pair. But this is all only needed for nodes that // have distinct policies for channels to the same peer. - modifiedPolicy := *bestPolicy - modifiedPolicy.TimeLockDelta = maxTimelock + policyCopy := *bestPolicy + modifiedEdge := unifiedEdge{policy: &policyCopy} + modifiedEdge.policy.TimeLockDelta = maxTimelock + modifiedEdge.capacity = maxCapMsat.ToSatoshis() - return &modifiedPolicy + return &modifiedEdge } // minAmt returns the minimum amount that can be forwarded on this connection. -func (u *unifiedPolicy) minAmt() lnwire.MilliSatoshi { +func (u *edgeUnifier) minAmt() lnwire.MilliSatoshi { min := lnwire.MaxMilliSatoshi for _, edge := range u.edges { - if edge.policy.MinHTLC < min { - min = edge.policy.MinHTLC - } + min = lntypes.Min(min, edge.policy.MinHTLC) } return min diff --git a/routing/unified_edges_test.go b/routing/unified_edges_test.go new file mode 100644 index 000000000..00a539034 --- /dev/null +++ b/routing/unified_edges_test.go @@ -0,0 +1,146 @@ +package routing + +import ( + "testing" + + "github.com/btcsuite/btcd/btcutil" + "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing/route" + "github.com/stretchr/testify/require" +) + +// TestNodeEdgeUnifier tests the composition of unified edges for nodes that +// have multiple channels between them. +func TestNodeEdgeUnifier(t *testing.T) { + t.Parallel() + + source := route.Vertex{1} + toNode := route.Vertex{2} + fromNode := route.Vertex{3} + bandwidthHints := &mockBandwidthHints{} + + // Add two channels between the pair of nodes. + p1 := channeldb.CachedEdgePolicy{ + FeeProportionalMillionths: 100000, + FeeBaseMSat: 30, + TimeLockDelta: 60, + MessageFlags: lnwire.ChanUpdateOptionMaxHtlc, + MaxHTLC: 5000, + MinHTLC: 100, + } + p2 := channeldb.CachedEdgePolicy{ + FeeProportionalMillionths: 190000, + FeeBaseMSat: 10, + TimeLockDelta: 40, + MessageFlags: lnwire.ChanUpdateOptionMaxHtlc, + MaxHTLC: 4000, + MinHTLC: 100, + } + c1 := btcutil.Amount(7) + c2 := btcutil.Amount(8) + + unifierFilled := newNodeEdgeUnifier(source, toNode, nil) + unifierFilled.addPolicy(fromNode, &p1, c1) + unifierFilled.addPolicy(fromNode, &p2, c2) + + unifierNoCapacity := newNodeEdgeUnifier(source, toNode, nil) + unifierNoCapacity.addPolicy(fromNode, &p1, 0) + unifierNoCapacity.addPolicy(fromNode, &p2, 0) + + unifierNoInfo := newNodeEdgeUnifier(source, toNode, nil) + unifierNoInfo.addPolicy(fromNode, &channeldb.CachedEdgePolicy{}, 0) + + tests := []struct { + name string + unifier *nodeEdgeUnifier + amount lnwire.MilliSatoshi + expectedFeeBase lnwire.MilliSatoshi + expectedFeeRate lnwire.MilliSatoshi + expectedTimeLock uint16 + expectNoPolicy bool + expectedCapacity btcutil.Amount + }{ + { + name: "amount below min htlc", + unifier: unifierFilled, + amount: 50, + expectNoPolicy: true, + }, + { + name: "amount above max htlc", + unifier: unifierFilled, + amount: 5500, + expectNoPolicy: true, + }, + // For 200 msat, p1 yields the highest fee. Use that policy to + // forward, because it will also match p2 in case p1 does not + // have enough balance. + { + name: "use p1 with highest fee", + unifier: unifierFilled, + amount: 200, + expectedFeeBase: p1.FeeBaseMSat, + expectedFeeRate: p1.FeeProportionalMillionths, + expectedTimeLock: p1.TimeLockDelta, + expectedCapacity: c2, + }, + // For 400 sat, p2 yields the highest fee. Use that policy to + // forward, because it will also match p1 in case p2 does not + // have enough balance. In order to match p1, it needs to have + // p1's time lock delta. + { + name: "use p2 with highest fee", + unifier: unifierFilled, + amount: 400, + expectedFeeBase: p2.FeeBaseMSat, + expectedFeeRate: p2.FeeProportionalMillionths, + expectedTimeLock: p1.TimeLockDelta, + expectedCapacity: c2, + }, + // If there's no capacity info present, we fall back to the max + // maxHTLC value. + { + name: "no capacity info", + unifier: unifierNoCapacity, + amount: 400, + expectedFeeBase: p2.FeeBaseMSat, + expectedFeeRate: p2.FeeProportionalMillionths, + expectedTimeLock: p1.TimeLockDelta, + expectedCapacity: p1.MaxHTLC.ToSatoshis(), + }, + { + name: "no info", + unifier: unifierNoInfo, + expectedCapacity: 0, + }, + } + + for _, test := range tests { + test := test + + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + edge := test.unifier.edgeUnifiers[fromNode].getEdge( + test.amount, bandwidthHints, + ) + + if test.expectNoPolicy { + require.Nil(t, edge, "expected no policy") + + return + } + + policy := edge.policy + require.Equal(t, test.expectedFeeBase, + policy.FeeBaseMSat, "base fee") + require.Equal(t, test.expectedFeeRate, + policy.FeeProportionalMillionths, "fee rate") + require.Equal(t, test.expectedTimeLock, + policy.TimeLockDelta, "timelock") + require.Equal(t, test.expectedCapacity, edge.capacity, + "capacity") + }) + } +} diff --git a/routing/unified_policies_test.go b/routing/unified_policies_test.go deleted file mode 100644 index abdc56b62..000000000 --- a/routing/unified_policies_test.go +++ /dev/null @@ -1,91 +0,0 @@ -package routing - -import ( - "testing" - - "github.com/lightningnetwork/lnd/channeldb" - "github.com/lightningnetwork/lnd/lnwire" - "github.com/lightningnetwork/lnd/routing/route" -) - -// TestUnifiedPolicies tests the composition of unified policies for nodes that -// have multiple channels between them. -func TestUnifiedPolicies(t *testing.T) { - source := route.Vertex{1} - toNode := route.Vertex{2} - fromNode := route.Vertex{3} - - bandwidthHints := &mockBandwidthHints{} - - u := newUnifiedPolicies(source, toNode, nil) - - // Add two channels between the pair of nodes. - p1 := channeldb.CachedEdgePolicy{ - FeeProportionalMillionths: 100000, - FeeBaseMSat: 30, - TimeLockDelta: 60, - MessageFlags: lnwire.ChanUpdateOptionMaxHtlc, - MaxHTLC: 500, - MinHTLC: 100, - } - p2 := channeldb.CachedEdgePolicy{ - FeeProportionalMillionths: 190000, - FeeBaseMSat: 10, - TimeLockDelta: 40, - MessageFlags: lnwire.ChanUpdateOptionMaxHtlc, - MaxHTLC: 400, - MinHTLC: 100, - } - u.addPolicy(fromNode, &p1, 7) - u.addPolicy(fromNode, &p2, 7) - - checkPolicy := func(policy *channeldb.CachedEdgePolicy, - feeBase lnwire.MilliSatoshi, feeRate lnwire.MilliSatoshi, - timeLockDelta uint16) { - - t.Helper() - - if policy.FeeBaseMSat != feeBase { - t.Fatalf("expected fee base %v, got %v", - feeBase, policy.FeeBaseMSat) - } - - if policy.TimeLockDelta != timeLockDelta { - t.Fatalf("expected fee base %v, got %v", - timeLockDelta, policy.TimeLockDelta) - } - - if policy.FeeProportionalMillionths != feeRate { - t.Fatalf("expected fee rate %v, got %v", - feeRate, policy.FeeProportionalMillionths) - } - } - - policy := u.policies[fromNode].getPolicy(50, bandwidthHints) - if policy != nil { - t.Fatal("expected no policy for amt below min htlc") - } - - policy = u.policies[fromNode].getPolicy(550, bandwidthHints) - if policy != nil { - t.Fatal("expected no policy for amt above max htlc") - } - - // For 200 sat, p1 yields the highest fee. Use that policy to forward, - // because it will also match p2 in case p1 does not have enough - // balance. - policy = u.policies[fromNode].getPolicy(200, bandwidthHints) - checkPolicy( - policy, p1.FeeBaseMSat, p1.FeeProportionalMillionths, - p1.TimeLockDelta, - ) - - // For 400 sat, p2 yields the highest fee. Use that policy to forward, - // because it will also match p1 in case p2 does not have enough - // balance. In order to match p1, it needs to have p1's time lock delta. - policy = u.policies[fromNode].getPolicy(400, bandwidthHints) - checkPolicy( - policy, p2.FeeBaseMSat, p2.FeeProportionalMillionths, - p1.TimeLockDelta, - ) -} diff --git a/rpcserver.go b/rpcserver.go index 4c3062c64..95e001022 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -689,6 +689,7 @@ func (r *rpcServer) addDeps(s *server, macService *macaroons.Service, return err } graph := s.graphDB + routerBackend := &routerrpc.RouterBackend{ SelfNode: selfNode.PubKeyBytes, FetchChannelCapacity: func(chanID uint64) (btcutil.Amount, @@ -700,6 +701,28 @@ func (r *rpcServer) addDeps(s *server, macService *macaroons.Service, } return info.Capacity, nil }, + FetchAmountPairCapacity: func(nodeFrom, nodeTo route.Vertex, + amount lnwire.MilliSatoshi) (btcutil.Amount, error) { + + routingGraph, err := routing.NewCachedGraph( + selfNode, graph, + ) + if err != nil { + return 0, err + } + defer func() { + closeErr := routingGraph.Close() + if closeErr != nil { + rpcsLog.Errorf("not able to close "+ + "routing graph tx: %v", + closeErr) + } + }() + + return routingGraph.FetchAmountPairCapacity( + nodeFrom, nodeTo, amount, + ) + }, FetchChannelEndpoints: func(chanID uint64) (route.Vertex, route.Vertex, error) {