Merge pull request #6857 from bitromortac/2208-apriori-capacity

pathfinding: capacity-dependent apriori model probability
This commit is contained in:
Olaoluwa Osuntokun authored and GitHub committed 2022-12-12 19:07:39 -08:00
commit d468391d95
32 files changed
+773 -348

No files matched your search

+2 -1
View File
@@ -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 {
@@ -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
+5 -5
View File
@@ -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
+12 -8
View File
@@ -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=
+2 -2
View File
@@ -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);
+1 -1
View File
@@ -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": {
+11 -32
View File
@@ -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) (
+14 -7
View File
@@ -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
}
+4 -4
View File
@@ -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
+1 -28
View File
@@ -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,
@@ -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
}
+1 -1
View File
@@ -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",
+26
View File
@@ -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
}
+25
View File
@@ -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))
}
+40 -2
View File
@@ -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
}
+5 -2
View File
@@ -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
+11 -6
View File
@@ -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")
}
+25
View File
@@ -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 {
+4 -3
View File
@@ -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)
}
+33 -31
View File
@@ -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
+15 -9
View File
@@ -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
}
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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)
}
+5 -6
View File
@@ -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(
+80 -7
View File
@@ -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,
+129 -11
View File
@@ -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)
})
}
}
+30 -31
View File
@@ -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.
+3 -3
View File
@@ -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,
@@ -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
+146
View File
@@ -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")
})
}
}
-91
View File
@@ -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,
)
}
+23
View File
@@ -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) {