Merge pull request #6857 from bitromortac/2208-apriori-capacity
pathfinding: capacity-dependent apriori model probability
This commit is contained in:
32 files changed
+773
-348
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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) (
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
|
||||
Reference in new issue
Block a user