From 29c70182f7646301aa8c7df578b608fab498f5f1 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Tue, 19 Mar 2019 15:07:05 +0100 Subject: [PATCH 01/11] routing/test: sort nodes for channel graph This commit ensures that channel endpoints in EdgeInfo are ordered node1 < node2 to not violate assumptions being made by dependent code. --- routing/pathfind_test.go | 42 +++++++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/routing/pathfind_test.go b/routing/pathfind_test.go index 7ce114c29071..fb5ca0a57bca 100644 --- a/routing/pathfind_test.go +++ b/routing/pathfind_test.go @@ -477,6 +477,16 @@ func createTestGraphFromChannels(testChannels []*testChannel) (*testGraphInstanc Index: 0, } + // Sort nodes + node1 := testChannel.Node1 + node2 := testChannel.Node2 + node1Vertex := aliasMap[node1.Alias] + node2Vertex := aliasMap[node2.Alias] + if bytes.Compare(node1Vertex[:], node2Vertex[:]) == 1 { + node1, node2 = node2, node1 + node1Vertex, node2Vertex = node2Vertex, node1Vertex + } + // We first insert the existence of the edge between the two // nodes. edgeInfo := channeldb.ChannelEdgeInfo{ @@ -485,10 +495,10 @@ func createTestGraphFromChannels(testChannels []*testChannel) (*testGraphInstanc ChannelPoint: *fundingPoint, Capacity: testChannel.Capacity, - NodeKey1Bytes: aliasMap[testChannel.Node1.Alias], - BitcoinKey1Bytes: aliasMap[testChannel.Node1.Alias], - NodeKey2Bytes: aliasMap[testChannel.Node2.Alias], - BitcoinKey2Bytes: aliasMap[testChannel.Node2.Alias], + NodeKey1Bytes: node1Vertex, + BitcoinKey1Bytes: node1Vertex, + NodeKey2Bytes: node2Vertex, + BitcoinKey2Bytes: node2Vertex, } err = graph.AddChannelEdge(&edgeInfo) @@ -510,12 +520,12 @@ func createTestGraphFromChannels(testChannels []*testChannel) (*testGraphInstanc MessageFlags: msgFlags, ChannelFlags: channelFlags, ChannelID: channelID, - LastUpdate: testChannel.Node1.LastUpdate, - TimeLockDelta: testChannel.Node1.Expiry, - MinHTLC: testChannel.Node1.MinHTLC, - MaxHTLC: testChannel.Node1.MaxHTLC, - FeeBaseMSat: testChannel.Node1.FeeBaseMsat, - FeeProportionalMillionths: testChannel.Node1.FeeRate, + LastUpdate: node1.LastUpdate, + TimeLockDelta: node1.Expiry, + MinHTLC: node1.MinHTLC, + MaxHTLC: node1.MaxHTLC, + FeeBaseMSat: node1.FeeBaseMsat, + FeeProportionalMillionths: node1.FeeRate, } if err := graph.UpdateEdgePolicy(edgePolicy); err != nil { return nil, err @@ -536,12 +546,12 @@ func createTestGraphFromChannels(testChannels []*testChannel) (*testGraphInstanc MessageFlags: msgFlags, ChannelFlags: channelFlags, ChannelID: channelID, - LastUpdate: testChannel.Node2.LastUpdate, - TimeLockDelta: testChannel.Node2.Expiry, - MinHTLC: testChannel.Node2.MinHTLC, - MaxHTLC: testChannel.Node2.MaxHTLC, - FeeBaseMSat: testChannel.Node2.FeeBaseMsat, - FeeProportionalMillionths: testChannel.Node2.FeeRate, + LastUpdate: node2.LastUpdate, + TimeLockDelta: node2.Expiry, + MinHTLC: node2.MinHTLC, + MaxHTLC: node2.MaxHTLC, + FeeBaseMSat: node2.FeeBaseMsat, + FeeProportionalMillionths: node2.FeeRate, } if err := graph.UpdateEdgePolicy(edgePolicy); err != nil { return nil, err From 721ec8a0dd065b2a9811402af2e2f5266e760d99 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Fri, 25 Jan 2019 10:44:21 +0100 Subject: [PATCH 02/11] routing: Route String() method --- routing/route/route.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/routing/route/route.go b/routing/route/route.go index 9b010e771e84..fe1a39d1defc 100644 --- a/routing/route/route.go +++ b/routing/route/route.go @@ -3,6 +3,8 @@ package route import ( "encoding/binary" "fmt" + "strconv" + "strings" "github.com/btcsuite/btcd/btcec" sphinx "github.com/lightningnetwork/lightning-onion" @@ -176,3 +178,20 @@ func (r *Route) ToSphinxPath() (*sphinx.PaymentPath, error) { return &path, nil } + +// String returns a human readable representation of the route. +func (r *Route) String() string { + var b strings.Builder + + for i, hop := range r.Hops { + if i > 0 { + b.WriteString(",") + } + b.WriteString(strconv.FormatUint(hop.ChannelID, 10)) + } + + return fmt.Sprintf("amt=%v, fees=%v, tl=%v, chans=%v", + r.TotalAmount-r.TotalFees(), r.TotalFees(), r.TotalTimeLock, + b.String(), + ) +} From 91389cb472e02fa889fabcdd15504c72e8863f6a Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Thu, 9 May 2019 12:29:39 +0200 Subject: [PATCH 03/11] routing: export MissionControl --- routing/missioncontrol.go | 26 +++++++++++++------------- routing/payment_session.go | 2 +- routing/payment_session_test.go | 2 +- routing/router.go | 8 ++++---- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/routing/missioncontrol.go b/routing/missioncontrol.go index 0a8a48a1b7cf..ac03ceff67ca 100644 --- a/routing/missioncontrol.go +++ b/routing/missioncontrol.go @@ -31,7 +31,7 @@ const ( edgeDecay = time.Duration(time.Second * 5) ) -// missionControl contains state which summarizes the past attempts of HTLC +// MissionControl contains state which summarizes the past attempts of HTLC // routing by external callers when sending payments throughout the network. // missionControl remembers the outcome of these past routing attempts (success // and failure), and is able to provide hints/guidance to future HTLC routing @@ -43,7 +43,7 @@ const ( // to the view. Later sending attempts will then query the view for all the // vertexes/edges that should be ignored. Items in the view decay after a set // period of time, allowing the view to be dynamic w.r.t network changes. -type missionControl struct { +type MissionControl struct { // failedEdges maps a short channel ID to be pruned, to the time that // it was added to the prune view. Edges are added to this map if a // caller reports to missionControl a failure localized to that edge @@ -70,13 +70,13 @@ type missionControl struct { // TODO(roasbeef): also add favorable metrics for nodes } -// newMissionControl returns a new instance of missionControl. +// NewMissionControl returns a new instance of missionControl. // // TODO(roasbeef): persist memory -func newMissionControl(g *channeldb.ChannelGraph, selfNode *channeldb.LightningNode, - qb func(*channeldb.ChannelEdgeInfo) lnwire.MilliSatoshi) *missionControl { +func NewMissionControl(g *channeldb.ChannelGraph, selfNode *channeldb.LightningNode, + qb func(*channeldb.ChannelEdgeInfo) lnwire.MilliSatoshi) *MissionControl { - return &missionControl{ + return &MissionControl{ failedEdges: make(map[EdgeLocator]time.Time), failedVertexes: make(map[route.Vertex]time.Time), selfNode: selfNode, @@ -101,7 +101,7 @@ type graphPruneView struct { // prune view, it is to be ignored as a goroutine has had issues routing // through it successfully. Within this method the main view of the // missionControl is garbage collected as entries are detected to be "stale". -func (m *missionControl) GraphPruneView() graphPruneView { +func (m *MissionControl) graphPruneView() graphPruneView { // First, we'll grab the current time, this value will be used to // determine if an entry is stale or not. now := time.Now() @@ -154,10 +154,10 @@ func (m *missionControl) GraphPruneView() graphPruneView { // view from Mission Control. An optional set of routing hints can be provided // in order to populate additional edges to explore when finding a path to the // payment's destination. -func (m *missionControl) NewPaymentSession(routeHints [][]zpay32.HopHint, +func (m *MissionControl) newPaymentSession(routeHints [][]zpay32.HopHint, target route.Vertex) (*paymentSession, error) { - viewSnapshot := m.GraphPruneView() + viewSnapshot := m.graphPruneView() edges := make(map[route.Vertex][]*channeldb.ChannelEdgePolicy) @@ -231,11 +231,11 @@ func (m *missionControl) NewPaymentSession(routeHints [][]zpay32.HopHint, }, nil } -// NewPaymentSessionForRoute creates a new paymentSession instance that is just +// newPaymentSessionForRoute creates a new paymentSession instance that is just // used for failure reporting to missioncontrol. -func (m *missionControl) NewPaymentSessionForRoute(preBuiltRoute *route.Route) *paymentSession { +func (m *MissionControl) newPaymentSessionForRoute(preBuiltRoute *route.Route) *paymentSession { return &paymentSession{ - pruneViewSnapshot: m.GraphPruneView(), + pruneViewSnapshot: m.graphPruneView(), errFailedPolicyChans: make(map[EdgeLocator]struct{}), mc: m, preBuiltRoute: preBuiltRoute, @@ -279,7 +279,7 @@ func generateBandwidthHints(sourceNode *channeldb.LightningNode, // ResetHistory resets the history of missionControl returning it to a state as // if no payment attempts have been made. -func (m *missionControl) ResetHistory() { +func (m *MissionControl) ResetHistory() { m.Lock() m.failedEdges = make(map[EdgeLocator]time.Time) m.failedVertexes = make(map[route.Vertex]time.Time) diff --git a/routing/payment_session.go b/routing/payment_session.go index 459a8de2f898..4f9edba5d29e 100644 --- a/routing/payment_session.go +++ b/routing/payment_session.go @@ -30,7 +30,7 @@ type paymentSession struct { // require pruning, but any subsequent ones do. errFailedPolicyChans map[EdgeLocator]struct{} - mc *missionControl + mc *MissionControl preBuiltRoute *route.Route preBuiltRouteTried bool diff --git a/routing/payment_session_test.go b/routing/payment_session_test.go index 0f3133bc4721..f730bc576711 100644 --- a/routing/payment_session_test.go +++ b/routing/payment_session_test.go @@ -33,7 +33,7 @@ func TestRequestRoute(t *testing.T) { } session := &paymentSession{ - mc: &missionControl{ + mc: &MissionControl{ selfNode: &channeldb.LightningNode{}, }, pruneViewSnapshot: graphPruneView{}, diff --git a/routing/router.go b/routing/router.go index 75fe831dc687..582e1401389d 100644 --- a/routing/router.go +++ b/routing/router.go @@ -345,7 +345,7 @@ type ChannelRouter struct { // Each run will then take into account this set of pruned // vertexes/edges to reduce route failure and pass on graph information // gained to the next execution. - missionControl *missionControl + missionControl *MissionControl // channelEdgeMtx is a mutex we use to make sure we process only one // ChannelEdgePolicy at a time for a given channelID, to ensure @@ -388,7 +388,7 @@ func New(cfg Config) (*ChannelRouter, error) { quit: make(chan struct{}), } - r.missionControl = newMissionControl( + r.missionControl = NewMissionControl( cfg.Graph, selfNode, cfg.QueryBandwidth, ) @@ -1543,7 +1543,7 @@ func (r *ChannelRouter) SendPayment(payment *LightningPayment) ([32]byte, *route // Before starting the HTLC routing attempt, we'll create a fresh // payment session which will report our errors back to mission // control. - paySession, err := r.missionControl.NewPaymentSession( + paySession, err := r.missionControl.newPaymentSession( payment.RouteHints, payment.Target, ) if err != nil { @@ -1560,7 +1560,7 @@ func (r *ChannelRouter) SendToRoute(hash lntypes.Hash, route *route.Route) ( lntypes.Preimage, error) { // Create a payment session for just this route. - paySession := r.missionControl.NewPaymentSessionForRoute(route) + paySession := r.missionControl.newPaymentSessionForRoute(route) // Create a (mostly) dummy payment, as the created payment session is // not going to do path finding. From 17d15b3c904385bcc16ba94f1976b8092e1f43d1 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Fri, 10 May 2019 18:00:15 +0200 Subject: [PATCH 04/11] routerrpc: expose mission control reset rpc --- lnrpc/routerrpc/router.pb.go | 256 +++++++++++++++++++++--------- lnrpc/routerrpc/router.proto | 9 ++ lnrpc/routerrpc/router_backend.go | 2 + lnrpc/routerrpc/router_server.go | 14 ++ routing/router.go | 30 ++-- routing/router_test.go | 19 ++- rpcserver.go | 3 +- server.go | 67 ++++---- 8 files changed, 276 insertions(+), 124 deletions(-) diff --git a/lnrpc/routerrpc/router.pb.go b/lnrpc/routerrpc/router.pb.go index 031f0143c383..3b6c23ecb539 100644 --- a/lnrpc/routerrpc/router.pb.go +++ b/lnrpc/routerrpc/router.pb.go @@ -695,6 +695,68 @@ func (m *ChannelUpdate) GetFeeRate() uint32 { return 0 } +type ResetMissionControlRequest struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ResetMissionControlRequest) Reset() { *m = ResetMissionControlRequest{} } +func (m *ResetMissionControlRequest) String() string { return proto.CompactTextString(m) } +func (*ResetMissionControlRequest) ProtoMessage() {} +func (*ResetMissionControlRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_7a0613f69d37b0a5, []int{8} +} + +func (m *ResetMissionControlRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ResetMissionControlRequest.Unmarshal(m, b) +} +func (m *ResetMissionControlRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ResetMissionControlRequest.Marshal(b, m, deterministic) +} +func (m *ResetMissionControlRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_ResetMissionControlRequest.Merge(m, src) +} +func (m *ResetMissionControlRequest) XXX_Size() int { + return xxx_messageInfo_ResetMissionControlRequest.Size(m) +} +func (m *ResetMissionControlRequest) XXX_DiscardUnknown() { + xxx_messageInfo_ResetMissionControlRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_ResetMissionControlRequest proto.InternalMessageInfo + +type ResetMissionControlResponse struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ResetMissionControlResponse) Reset() { *m = ResetMissionControlResponse{} } +func (m *ResetMissionControlResponse) String() string { return proto.CompactTextString(m) } +func (*ResetMissionControlResponse) ProtoMessage() {} +func (*ResetMissionControlResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_7a0613f69d37b0a5, []int{9} +} + +func (m *ResetMissionControlResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ResetMissionControlResponse.Unmarshal(m, b) +} +func (m *ResetMissionControlResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ResetMissionControlResponse.Marshal(b, m, deterministic) +} +func (m *ResetMissionControlResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_ResetMissionControlResponse.Merge(m, src) +} +func (m *ResetMissionControlResponse) XXX_Size() int { + return xxx_messageInfo_ResetMissionControlResponse.Size(m) +} +func (m *ResetMissionControlResponse) XXX_DiscardUnknown() { + xxx_messageInfo_ResetMissionControlResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_ResetMissionControlResponse proto.InternalMessageInfo + func init() { proto.RegisterEnum("routerrpc.Failure_FailureCode", Failure_FailureCode_name, Failure_FailureCode_value) proto.RegisterType((*PaymentRequest)(nil), "routerrpc.PaymentRequest") @@ -705,86 +767,91 @@ func init() { proto.RegisterType((*SendToRouteResponse)(nil), "routerrpc.SendToRouteResponse") proto.RegisterType((*Failure)(nil), "routerrpc.Failure") proto.RegisterType((*ChannelUpdate)(nil), "routerrpc.ChannelUpdate") + proto.RegisterType((*ResetMissionControlRequest)(nil), "routerrpc.ResetMissionControlRequest") + proto.RegisterType((*ResetMissionControlResponse)(nil), "routerrpc.ResetMissionControlResponse") } func init() { proto.RegisterFile("routerrpc/router.proto", fileDescriptor_7a0613f69d37b0a5) } var fileDescriptor_7a0613f69d37b0a5 = []byte{ - // 1172 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0x56, 0xdd, 0x72, 0x1a, 0x37, - 0x14, 0x2e, 0xb1, 0xcd, 0xcf, 0x01, 0xec, 0xb5, 0x6c, 0x27, 0x98, 0xc4, 0x89, 0x43, 0x3b, 0xad, - 0xa7, 0xd3, 0xb1, 0xa7, 0x74, 0x92, 0xcb, 0x76, 0x36, 0x20, 0xea, 0x9d, 0xc0, 0x2e, 0xd5, 0x82, - 0x13, 0xb7, 0x17, 0x1a, 0x99, 0x95, 0x61, 0x6b, 0xf6, 0x27, 0xbb, 0xa2, 0xad, 0x5f, 0xa0, 0x0f, - 0xd4, 0x77, 0xe8, 0x43, 0xf4, 0x11, 0xfa, 0x16, 0x1d, 0x49, 0xbb, 0x18, 0x6c, 0xf7, 0x8a, 0xd5, - 0xf7, 0x1d, 0x7d, 0xd2, 0x39, 0x3a, 0x3f, 0xc0, 0xd3, 0x24, 0x5a, 0x08, 0x9e, 0x24, 0xf1, 0xe4, - 0x4c, 0x7f, 0x9d, 0xc6, 0x49, 0x24, 0x22, 0x54, 0x59, 0xe2, 0xcd, 0x4a, 0x12, 0x4f, 0x34, 0xda, - 0xfa, 0xbb, 0x00, 0xdb, 0x43, 0x76, 0x1b, 0xf0, 0x50, 0x10, 0xfe, 0x69, 0xc1, 0x53, 0x81, 0x9e, - 0x41, 0x29, 0x66, 0xb7, 0x34, 0xe1, 0x9f, 0x1a, 0x85, 0xe3, 0xc2, 0x49, 0x85, 0x14, 0x63, 0x76, - 0x4b, 0xf8, 0x27, 0xd4, 0x82, 0xfa, 0x35, 0xe7, 0x74, 0xee, 0x07, 0xbe, 0xa0, 0x29, 0x13, 0x8d, - 0x27, 0xc7, 0x85, 0x93, 0x0d, 0x52, 0xbd, 0xe6, 0xbc, 0x2f, 0x31, 0x97, 0x09, 0x74, 0x04, 0x30, - 0x99, 0x8b, 0xdf, 0xb4, 0x51, 0x63, 0xe3, 0xb8, 0x70, 0xb2, 0x45, 0x2a, 0x12, 0x51, 0x16, 0xe8, - 0x2b, 0xd8, 0x11, 0x7e, 0xc0, 0xa3, 0x85, 0xa0, 0x29, 0x9f, 0x44, 0xa1, 0x97, 0x36, 0x36, 0x95, - 0xcd, 0x76, 0x06, 0xbb, 0x1a, 0x45, 0xa7, 0xb0, 0x17, 0x2d, 0xc4, 0x34, 0xf2, 0xc3, 0x29, 0x9d, - 0xcc, 0x58, 0x18, 0xf2, 0x39, 0xf5, 0xbd, 0xc6, 0x96, 0x3a, 0x71, 0x37, 0xa7, 0x3a, 0x9a, 0xb1, - 0xbc, 0xd6, 0xaf, 0xb0, 0xb3, 0x74, 0x23, 0x8d, 0xa3, 0x30, 0xe5, 0xe8, 0x10, 0xca, 0xd2, 0x8f, - 0x19, 0x4b, 0x67, 0xca, 0x91, 0x1a, 0x91, 0x7e, 0x9d, 0xb3, 0x74, 0x86, 0x9e, 0x43, 0x25, 0x4e, - 0x38, 0xf5, 0x03, 0x36, 0xe5, 0xca, 0x8b, 0x1a, 0x29, 0xc7, 0x09, 0xb7, 0xe4, 0x1a, 0xbd, 0x82, - 0x6a, 0xac, 0xa5, 0x28, 0x4f, 0x12, 0xe5, 0x43, 0x85, 0x40, 0x06, 0xe1, 0x24, 0x69, 0x7d, 0x0f, - 0x3b, 0x44, 0xc6, 0xb2, 0xc7, 0x79, 0x1e, 0x33, 0x04, 0x9b, 0x1e, 0x4f, 0x45, 0x76, 0x8e, 0xfa, - 0x96, 0x71, 0x64, 0xc1, 0x6a, 0xa0, 0x8a, 0x2c, 0x90, 0x31, 0x6a, 0x79, 0x60, 0xdc, 0xed, 0xcf, - 0x2e, 0x7b, 0x02, 0x86, 0x7c, 0x1f, 0xe9, 0xae, 0x8c, 0x71, 0x20, 0x77, 0x15, 0xd4, 0xae, 0xed, - 0x0c, 0xef, 0x71, 0x3e, 0x48, 0x99, 0x40, 0x5f, 0xea, 0x10, 0xd2, 0x79, 0x34, 0xb9, 0xa1, 0x1e, - 0x9f, 0xb3, 0xdb, 0x4c, 0xbe, 0x2e, 0xe1, 0x7e, 0x34, 0xb9, 0xe9, 0x4a, 0xb0, 0xf5, 0x0b, 0x20, - 0x97, 0x87, 0xde, 0x28, 0x52, 0x67, 0xe5, 0x17, 0x7d, 0x0d, 0xb5, 0xdc, 0xb9, 0x95, 0xc0, 0xe4, - 0x0e, 0xab, 0xe0, 0xb4, 0x60, 0x4b, 0xa5, 0x8a, 0x92, 0xad, 0xb6, 0x6b, 0xa7, 0xf3, 0x50, 0xe6, - 0x8b, 0x96, 0xd1, 0x54, 0x8b, 0xc2, 0xde, 0x9a, 0x78, 0xe6, 0x45, 0x13, 0x64, 0x18, 0x75, 0x58, - 0x0b, 0xcb, 0xb0, 0xaa, 0x35, 0xfa, 0x06, 0x4a, 0xd7, 0xcc, 0x9f, 0x2f, 0x92, 0x5c, 0x18, 0x9d, - 0x2e, 0x33, 0xf2, 0xb4, 0xa7, 0x19, 0x92, 0x9b, 0xb4, 0xfe, 0x2c, 0x41, 0x29, 0x03, 0x51, 0x1b, - 0x36, 0x27, 0x91, 0xa7, 0x15, 0xb7, 0xdb, 0x2f, 0x1f, 0x6e, 0xcb, 0x7f, 0x3b, 0x91, 0xc7, 0x89, - 0xb2, 0x45, 0x6d, 0x38, 0xc8, 0xa4, 0x68, 0x1a, 0x2d, 0x92, 0x09, 0xa7, 0xf1, 0xe2, 0xea, 0x86, - 0xdf, 0x66, 0xaf, 0xbd, 0x97, 0x91, 0xae, 0xe2, 0x86, 0x8a, 0x42, 0x3f, 0xc0, 0x76, 0x9e, 0x6a, - 0x8b, 0xd8, 0x63, 0x82, 0xab, 0xb7, 0xaf, 0xb6, 0x1b, 0x2b, 0x27, 0x66, 0x19, 0x37, 0x56, 0x3c, - 0xa9, 0x4f, 0x56, 0x97, 0x32, 0xad, 0x66, 0x62, 0x3e, 0xd1, 0xaf, 0x27, 0xf3, 0x7a, 0x93, 0x94, - 0x25, 0xa0, 0xde, 0xad, 0x05, 0xf5, 0x28, 0xf4, 0xa3, 0x90, 0xa6, 0x33, 0x46, 0xdb, 0x6f, 0xde, - 0xaa, 0x5c, 0xae, 0x91, 0xaa, 0x02, 0xdd, 0x19, 0x6b, 0xbf, 0x79, 0x2b, 0x53, 0x4f, 0x55, 0x0f, - 0xff, 0x23, 0xf6, 0x93, 0xdb, 0x46, 0xf1, 0xb8, 0x70, 0x52, 0x27, 0xaa, 0xa0, 0xb0, 0x42, 0xd0, - 0x3e, 0x6c, 0x5d, 0xcf, 0xd9, 0x34, 0x6d, 0x94, 0x14, 0xa5, 0x17, 0xad, 0x7f, 0x36, 0xa1, 0xba, - 0x12, 0x02, 0x54, 0x83, 0x32, 0xc1, 0x2e, 0x26, 0x17, 0xb8, 0x6b, 0x7c, 0x86, 0x1a, 0xb0, 0x3f, - 0xb6, 0xdf, 0xdb, 0xce, 0x07, 0x9b, 0x0e, 0xcd, 0xcb, 0x01, 0xb6, 0x47, 0xf4, 0xdc, 0x74, 0xcf, - 0x8d, 0x02, 0x7a, 0x01, 0x0d, 0xcb, 0xee, 0x38, 0x84, 0xe0, 0xce, 0x68, 0xc9, 0x99, 0x03, 0x67, - 0x6c, 0x8f, 0x8c, 0x27, 0xe8, 0x15, 0x3c, 0xef, 0x59, 0xb6, 0xd9, 0xa7, 0x77, 0x36, 0x9d, 0xfe, - 0xe8, 0x82, 0xe2, 0x8f, 0x43, 0x8b, 0x5c, 0x1a, 0x1b, 0x8f, 0x19, 0x9c, 0x8f, 0xfa, 0x9d, 0x5c, - 0x61, 0x13, 0x1d, 0xc2, 0x81, 0x36, 0xd0, 0x5b, 0xe8, 0xc8, 0x71, 0xa8, 0xeb, 0x38, 0xb6, 0xb1, - 0x85, 0x76, 0xa1, 0x6e, 0xd9, 0x17, 0x66, 0xdf, 0xea, 0x52, 0x82, 0xcd, 0xfe, 0xc0, 0x28, 0xa2, - 0x3d, 0xd8, 0xb9, 0x6f, 0x57, 0x92, 0x12, 0xb9, 0x9d, 0x63, 0x5b, 0x8e, 0x4d, 0x2f, 0x30, 0x71, - 0x2d, 0xc7, 0x36, 0xca, 0xe8, 0x29, 0xa0, 0x75, 0xea, 0x7c, 0x60, 0x76, 0x8c, 0x0a, 0x3a, 0x80, - 0xdd, 0x75, 0xfc, 0x3d, 0xbe, 0x34, 0x40, 0x86, 0x41, 0x5f, 0x8c, 0xbe, 0xc3, 0x7d, 0xe7, 0x03, - 0x1d, 0x58, 0xb6, 0x35, 0x18, 0x0f, 0x8c, 0x2a, 0xda, 0x07, 0xa3, 0x87, 0x31, 0xb5, 0x6c, 0x77, - 0xdc, 0xeb, 0x59, 0x1d, 0x0b, 0xdb, 0x23, 0xa3, 0xa6, 0x4f, 0x7e, 0xcc, 0xf1, 0xba, 0xdc, 0xd0, - 0x39, 0x37, 0x6d, 0x1b, 0xf7, 0x69, 0xd7, 0x72, 0xcd, 0x77, 0x7d, 0xdc, 0x35, 0xb6, 0xd1, 0x11, - 0x1c, 0x8e, 0xf0, 0x60, 0xe8, 0x10, 0x93, 0x5c, 0xd2, 0x9c, 0xef, 0x99, 0x56, 0x7f, 0x4c, 0xb0, - 0xb1, 0x83, 0x5e, 0xc3, 0x11, 0xc1, 0x3f, 0x8d, 0x2d, 0x82, 0xbb, 0xd4, 0x76, 0xba, 0x98, 0xf6, - 0xb0, 0x39, 0x1a, 0x13, 0x4c, 0x07, 0x96, 0xeb, 0x5a, 0xf6, 0x8f, 0x86, 0x81, 0xbe, 0x80, 0xe3, - 0xa5, 0xc9, 0x52, 0xe0, 0x9e, 0xd5, 0xae, 0xf4, 0x2f, 0x7f, 0x4f, 0x1b, 0x7f, 0x1c, 0xd1, 0x21, - 0xc6, 0xc4, 0x40, 0xa8, 0x09, 0x4f, 0xef, 0x8e, 0xd7, 0x07, 0x64, 0x67, 0xef, 0x49, 0x6e, 0x88, - 0xc9, 0xc0, 0xb4, 0xe5, 0x03, 0xaf, 0x71, 0xfb, 0xf2, 0xda, 0x77, 0xdc, 0xfd, 0x6b, 0x1f, 0xb4, - 0xfe, 0x7a, 0x02, 0xf5, 0xb5, 0xa4, 0x47, 0x2f, 0xa0, 0x92, 0xfa, 0xd3, 0x90, 0x09, 0x59, 0xca, - 0xba, 0xca, 0xef, 0x00, 0x35, 0x00, 0x66, 0xcc, 0x0f, 0x75, 0x7b, 0xd1, 0xd5, 0x56, 0x51, 0x88, - 0x6a, 0x2e, 0xcf, 0xa0, 0x24, 0x6b, 0x46, 0xf6, 0xf2, 0x0d, 0x55, 0x20, 0x45, 0xb9, 0xb4, 0x3c, - 0xa9, 0x2a, 0xfb, 0x57, 0x2a, 0x58, 0x10, 0xab, 0xda, 0xa9, 0x93, 0x3b, 0x00, 0x7d, 0x0e, 0x79, - 0xa9, 0x51, 0x9d, 0xff, 0x5b, 0xca, 0xa2, 0x96, 0x81, 0x3d, 0x89, 0x3d, 0xe8, 0x8c, 0x82, 0x65, - 0x15, 0xb4, 0xda, 0x19, 0x05, 0x43, 0x5f, 0xc3, 0xae, 0x2e, 0x53, 0x3f, 0xf4, 0x83, 0x45, 0xa0, - 0xcb, 0xb5, 0xa4, 0x6e, 0xb3, 0xa3, 0xca, 0x55, 0xe3, 0xaa, 0x6a, 0x0f, 0xa1, 0x7c, 0xc5, 0x52, - 0x2e, 0x9b, 0x72, 0xa3, 0xac, 0xc4, 0x4a, 0x72, 0xdd, 0xe3, 0x6a, 0xbe, 0xc8, 0x56, 0x9d, 0xc8, - 0x46, 0x51, 0xd1, 0xd4, 0x35, 0xe7, 0x84, 0x09, 0xde, 0xfe, 0xb7, 0x00, 0x45, 0xd5, 0x19, 0x13, - 0xd4, 0x85, 0xaa, 0xec, 0x94, 0xd9, 0x70, 0x42, 0x87, 0x2b, 0xbd, 0x64, 0x7d, 0xee, 0x36, 0x9b, - 0x8f, 0x51, 0x59, 0x63, 0x7d, 0x0f, 0x06, 0x4e, 0x85, 0x1f, 0xc8, 0xa6, 0x93, 0x8d, 0x0e, 0xb4, - 0x6a, 0x7f, 0x6f, 0x1e, 0x35, 0x9f, 0x3f, 0xca, 0x65, 0x62, 0x7d, 0x7d, 0xa5, 0xac, 0x79, 0xa3, - 0xa3, 0x15, 0xdb, 0x87, 0x13, 0xa3, 0xf9, 0xf2, 0xff, 0x68, 0xad, 0xf6, 0xee, 0xdb, 0x9f, 0xcf, - 0xa6, 0xbe, 0x98, 0x2d, 0xae, 0x4e, 0x27, 0x51, 0x70, 0x36, 0xf7, 0xa7, 0x33, 0x11, 0xfa, 0xe1, - 0x34, 0xe4, 0xe2, 0xf7, 0x28, 0xb9, 0x39, 0x9b, 0x87, 0xde, 0x99, 0x1a, 0x20, 0x67, 0x4b, 0x99, - 0xab, 0xa2, 0xfa, 0xef, 0xf1, 0xdd, 0x7f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x6d, 0x7d, 0x2a, 0xf5, - 0xab, 0x08, 0x00, 0x00, + // 1220 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x56, 0xdd, 0x72, 0xdb, 0xb6, + 0x12, 0x3e, 0xf2, 0x8f, 0x7e, 0x56, 0x92, 0x4d, 0xc3, 0x76, 0x22, 0xcb, 0x71, 0xe2, 0xf0, 0x9c, + 0x93, 0x7a, 0x3a, 0x1d, 0x7b, 0xaa, 0x4e, 0x72, 0xd9, 0x0e, 0x23, 0x41, 0x35, 0x27, 0x12, 0xa9, + 0x82, 0x92, 0x13, 0xb7, 0x17, 0x18, 0x58, 0x84, 0x25, 0xd6, 0x12, 0xc9, 0x90, 0x50, 0x5b, 0xbf, + 0x40, 0x1f, 0xa8, 0xef, 0xd0, 0xeb, 0x5e, 0xf7, 0x6d, 0x3a, 0x00, 0x48, 0x59, 0x76, 0x9c, 0xe9, + 0x95, 0x88, 0xef, 0x5b, 0xec, 0x62, 0x17, 0xbb, 0x1f, 0x04, 0x4f, 0x92, 0x68, 0x21, 0x78, 0x92, + 0xc4, 0xe3, 0x33, 0xfd, 0x75, 0x1a, 0x27, 0x91, 0x88, 0x50, 0x65, 0x89, 0x37, 0x2b, 0x49, 0x3c, + 0xd6, 0xa8, 0xf9, 0x67, 0x01, 0xb6, 0x06, 0xec, 0x76, 0xce, 0x43, 0x41, 0xf8, 0xc7, 0x05, 0x4f, + 0x05, 0x7a, 0x0a, 0xa5, 0x98, 0xdd, 0xd2, 0x84, 0x7f, 0x6c, 0x14, 0x8e, 0x0b, 0x27, 0x15, 0x52, + 0x8c, 0xd9, 0x2d, 0xe1, 0x1f, 0x91, 0x09, 0xf5, 0x6b, 0xce, 0xe9, 0x2c, 0x98, 0x07, 0x82, 0xa6, + 0x4c, 0x34, 0xd6, 0x8e, 0x0b, 0x27, 0xeb, 0xa4, 0x7a, 0xcd, 0x79, 0x4f, 0x62, 0x1e, 0x13, 0xe8, + 0x08, 0x60, 0x3c, 0x13, 0xbf, 0x68, 0xa3, 0xc6, 0xfa, 0x71, 0xe1, 0x64, 0x93, 0x54, 0x24, 0xa2, + 0x2c, 0xd0, 0x17, 0xb0, 0x2d, 0x82, 0x39, 0x8f, 0x16, 0x82, 0xa6, 0x7c, 0x1c, 0x85, 0x7e, 0xda, + 0xd8, 0x50, 0x36, 0x5b, 0x19, 0xec, 0x69, 0x14, 0x9d, 0xc2, 0x6e, 0xb4, 0x10, 0x93, 0x28, 0x08, + 0x27, 0x74, 0x3c, 0x65, 0x61, 0xc8, 0x67, 0x34, 0xf0, 0x1b, 0x9b, 0x2a, 0xe2, 0x4e, 0x4e, 0xb5, + 0x35, 0x63, 0xfb, 0xe6, 0xcf, 0xb0, 0xbd, 0x4c, 0x23, 0x8d, 0xa3, 0x30, 0xe5, 0xe8, 0x00, 0xca, + 0x32, 0x8f, 0x29, 0x4b, 0xa7, 0x2a, 0x91, 0x1a, 0x91, 0x79, 0x9d, 0xb3, 0x74, 0x8a, 0x0e, 0xa1, + 0x12, 0x27, 0x9c, 0x06, 0x73, 0x36, 0xe1, 0x2a, 0x8b, 0x1a, 0x29, 0xc7, 0x09, 0xb7, 0xe5, 0x1a, + 0xbd, 0x80, 0x6a, 0xac, 0x5d, 0x51, 0x9e, 0x24, 0x2a, 0x87, 0x0a, 0x81, 0x0c, 0xc2, 0x49, 0x62, + 0x7e, 0x0b, 0xdb, 0x44, 0xd6, 0xb2, 0xcb, 0x79, 0x5e, 0x33, 0x04, 0x1b, 0x3e, 0x4f, 0x45, 0x16, + 0x47, 0x7d, 0xcb, 0x3a, 0xb2, 0xf9, 0x6a, 0xa1, 0x8a, 0x6c, 0x2e, 0x6b, 0x64, 0xfa, 0x60, 0xdc, + 0xed, 0xcf, 0x0e, 0x7b, 0x02, 0x86, 0xbc, 0x1f, 0x99, 0xae, 0xac, 0xf1, 0x5c, 0xee, 0x2a, 0xa8, + 0x5d, 0x5b, 0x19, 0xde, 0xe5, 0xbc, 0x9f, 0x32, 0x81, 0x5e, 0xe9, 0x12, 0xd2, 0x59, 0x34, 0xbe, + 0xa1, 0x3e, 0x9f, 0xb1, 0xdb, 0xcc, 0x7d, 0x5d, 0xc2, 0xbd, 0x68, 0x7c, 0xd3, 0x91, 0xa0, 0xf9, + 0x13, 0x20, 0x8f, 0x87, 0xfe, 0x30, 0x52, 0xb1, 0xf2, 0x83, 0xbe, 0x84, 0x5a, 0x9e, 0xdc, 0x4a, + 0x61, 0xf2, 0x84, 0x55, 0x71, 0x4c, 0xd8, 0x54, 0xad, 0xa2, 0xdc, 0x56, 0x5b, 0xb5, 0xd3, 0x59, + 0x28, 0xfb, 0x45, 0xbb, 0xd1, 0x94, 0x49, 0x61, 0xf7, 0x9e, 0xf3, 0x2c, 0x8b, 0x26, 0xc8, 0x32, + 0xea, 0xb2, 0x16, 0x96, 0x65, 0x55, 0x6b, 0xf4, 0x15, 0x94, 0xae, 0x59, 0x30, 0x5b, 0x24, 0xb9, + 0x63, 0x74, 0xba, 0xec, 0xc8, 0xd3, 0xae, 0x66, 0x48, 0x6e, 0x62, 0xfe, 0x5e, 0x82, 0x52, 0x06, + 0xa2, 0x16, 0x6c, 0x8c, 0x23, 0x5f, 0x7b, 0xdc, 0x6a, 0x3d, 0xff, 0x74, 0x5b, 0xfe, 0xdb, 0x8e, + 0x7c, 0x4e, 0x94, 0x2d, 0x6a, 0xc1, 0x7e, 0xe6, 0x8a, 0xa6, 0xd1, 0x22, 0x19, 0x73, 0x1a, 0x2f, + 0xae, 0x6e, 0xf8, 0x6d, 0x76, 0xdb, 0xbb, 0x19, 0xe9, 0x29, 0x6e, 0xa0, 0x28, 0xf4, 0x1d, 0x6c, + 0xe5, 0xad, 0xb6, 0x88, 0x7d, 0x26, 0xb8, 0xba, 0xfb, 0x6a, 0xab, 0xb1, 0x12, 0x31, 0xeb, 0xb8, + 0x91, 0xe2, 0x49, 0x7d, 0xbc, 0xba, 0x94, 0x6d, 0x35, 0x15, 0xb3, 0xb1, 0xbe, 0x3d, 0xd9, 0xd7, + 0x1b, 0xa4, 0x2c, 0x01, 0x75, 0x6f, 0x26, 0xd4, 0xa3, 0x30, 0x88, 0x42, 0x9a, 0x4e, 0x19, 0x6d, + 0xbd, 0x7e, 0xa3, 0x7a, 0xb9, 0x46, 0xaa, 0x0a, 0xf4, 0xa6, 0xac, 0xf5, 0xfa, 0x8d, 0x6c, 0x3d, + 0x35, 0x3d, 0xfc, 0xb7, 0x38, 0x48, 0x6e, 0x1b, 0xc5, 0xe3, 0xc2, 0x49, 0x9d, 0xa8, 0x81, 0xc2, + 0x0a, 0x41, 0x7b, 0xb0, 0x79, 0x3d, 0x63, 0x93, 0xb4, 0x51, 0x52, 0x94, 0x5e, 0x98, 0x7f, 0x6f, + 0x40, 0x75, 0xa5, 0x04, 0xa8, 0x06, 0x65, 0x82, 0x3d, 0x4c, 0x2e, 0x70, 0xc7, 0xf8, 0x0f, 0x6a, + 0xc0, 0xde, 0xc8, 0x79, 0xe7, 0xb8, 0xef, 0x1d, 0x3a, 0xb0, 0x2e, 0xfb, 0xd8, 0x19, 0xd2, 0x73, + 0xcb, 0x3b, 0x37, 0x0a, 0xe8, 0x19, 0x34, 0x6c, 0xa7, 0xed, 0x12, 0x82, 0xdb, 0xc3, 0x25, 0x67, + 0xf5, 0xdd, 0x91, 0x33, 0x34, 0xd6, 0xd0, 0x0b, 0x38, 0xec, 0xda, 0x8e, 0xd5, 0xa3, 0x77, 0x36, + 0xed, 0xde, 0xf0, 0x82, 0xe2, 0x0f, 0x03, 0x9b, 0x5c, 0x1a, 0xeb, 0x8f, 0x19, 0x9c, 0x0f, 0x7b, + 0xed, 0xdc, 0xc3, 0x06, 0x3a, 0x80, 0x7d, 0x6d, 0xa0, 0xb7, 0xd0, 0xa1, 0xeb, 0x52, 0xcf, 0x75, + 0x1d, 0x63, 0x13, 0xed, 0x40, 0xdd, 0x76, 0x2e, 0xac, 0x9e, 0xdd, 0xa1, 0x04, 0x5b, 0xbd, 0xbe, + 0x51, 0x44, 0xbb, 0xb0, 0xfd, 0xd0, 0xae, 0x24, 0x5d, 0xe4, 0x76, 0xae, 0x63, 0xbb, 0x0e, 0xbd, + 0xc0, 0xc4, 0xb3, 0x5d, 0xc7, 0x28, 0xa3, 0x27, 0x80, 0xee, 0x53, 0xe7, 0x7d, 0xab, 0x6d, 0x54, + 0xd0, 0x3e, 0xec, 0xdc, 0xc7, 0xdf, 0xe1, 0x4b, 0x03, 0x64, 0x19, 0xf4, 0xc1, 0xe8, 0x5b, 0xdc, + 0x73, 0xdf, 0xd3, 0xbe, 0xed, 0xd8, 0xfd, 0x51, 0xdf, 0xa8, 0xa2, 0x3d, 0x30, 0xba, 0x18, 0x53, + 0xdb, 0xf1, 0x46, 0xdd, 0xae, 0xdd, 0xb6, 0xb1, 0x33, 0x34, 0x6a, 0x3a, 0xf2, 0x63, 0x89, 0xd7, + 0xe5, 0x86, 0xf6, 0xb9, 0xe5, 0x38, 0xb8, 0x47, 0x3b, 0xb6, 0x67, 0xbd, 0xed, 0xe1, 0x8e, 0xb1, + 0x85, 0x8e, 0xe0, 0x60, 0x88, 0xfb, 0x03, 0x97, 0x58, 0xe4, 0x92, 0xe6, 0x7c, 0xd7, 0xb2, 0x7b, + 0x23, 0x82, 0x8d, 0x6d, 0xf4, 0x12, 0x8e, 0x08, 0xfe, 0x61, 0x64, 0x13, 0xdc, 0xa1, 0x8e, 0xdb, + 0xc1, 0xb4, 0x8b, 0xad, 0xe1, 0x88, 0x60, 0xda, 0xb7, 0x3d, 0xcf, 0x76, 0xbe, 0x37, 0x0c, 0xf4, + 0x3f, 0x38, 0x5e, 0x9a, 0x2c, 0x1d, 0x3c, 0xb0, 0xda, 0x91, 0xf9, 0xe5, 0xf7, 0xe9, 0xe0, 0x0f, + 0x43, 0x3a, 0xc0, 0x98, 0x18, 0x08, 0x35, 0xe1, 0xc9, 0x5d, 0x78, 0x1d, 0x20, 0x8b, 0xbd, 0x2b, + 0xb9, 0x01, 0x26, 0x7d, 0xcb, 0x91, 0x17, 0x7c, 0x8f, 0xdb, 0x93, 0xc7, 0xbe, 0xe3, 0x1e, 0x1e, + 0x7b, 0xdf, 0xfc, 0x63, 0x0d, 0xea, 0xf7, 0x9a, 0x1e, 0x3d, 0x83, 0x4a, 0x1a, 0x4c, 0x42, 0x26, + 0xe4, 0x28, 0xeb, 0x29, 0xbf, 0x03, 0xd4, 0x03, 0x30, 0x65, 0x41, 0xa8, 0xe5, 0x45, 0x4f, 0x5b, + 0x45, 0x21, 0x4a, 0x5c, 0x9e, 0x42, 0x49, 0xce, 0x8c, 0xd4, 0xf2, 0x75, 0x35, 0x20, 0x45, 0xb9, + 0xb4, 0x7d, 0xe9, 0x55, 0xea, 0x57, 0x2a, 0xd8, 0x3c, 0x56, 0xb3, 0x53, 0x27, 0x77, 0x00, 0xfa, + 0x2f, 0xe4, 0xa3, 0x46, 0x75, 0xff, 0x6f, 0x2a, 0x8b, 0x5a, 0x06, 0x76, 0x25, 0xf6, 0x89, 0x32, + 0x0a, 0x96, 0x4d, 0xd0, 0xaa, 0x32, 0x0a, 0x86, 0xbe, 0x84, 0x1d, 0x3d, 0xa6, 0x41, 0x18, 0xcc, + 0x17, 0x73, 0x3d, 0xae, 0x25, 0x75, 0x9a, 0x6d, 0x35, 0xae, 0x1a, 0x57, 0x53, 0x7b, 0x00, 0xe5, + 0x2b, 0x96, 0x72, 0x29, 0xca, 0x8d, 0xb2, 0x72, 0x56, 0x92, 0xeb, 0x2e, 0x57, 0xef, 0x8b, 0x94, + 0xea, 0x44, 0x0a, 0x45, 0x45, 0x53, 0xd7, 0x9c, 0x13, 0x26, 0xb8, 0xf9, 0x0c, 0x9a, 0x84, 0xa7, + 0x5c, 0xf4, 0x83, 0x34, 0x0d, 0xa2, 0xb0, 0x1d, 0x85, 0x22, 0x89, 0x66, 0x99, 0x06, 0x9b, 0x47, + 0x70, 0xf8, 0x28, 0xab, 0x45, 0xb4, 0xf5, 0xd7, 0x1a, 0x14, 0x95, 0xac, 0x26, 0xa8, 0x03, 0x55, + 0x29, 0xb3, 0xd9, 0xcb, 0x86, 0x0e, 0x56, 0x84, 0xe8, 0xfe, 0xa3, 0xdd, 0x6c, 0x3e, 0x46, 0x65, + 0xaa, 0xfc, 0x0e, 0x0c, 0x9c, 0x8a, 0x60, 0x2e, 0x15, 0x2b, 0x7b, 0x77, 0xd0, 0xaa, 0xfd, 0x83, + 0xc7, 0xac, 0x79, 0xf8, 0x28, 0x97, 0x39, 0xeb, 0xe9, 0x23, 0x65, 0xca, 0x8f, 0x8e, 0x56, 0x6c, + 0x3f, 0x7d, 0x6e, 0x9a, 0xcf, 0x3f, 0x47, 0x67, 0xde, 0x7c, 0xd8, 0x7d, 0xa4, 0x14, 0xe8, 0xff, + 0xab, 0x27, 0xf8, 0x6c, 0x21, 0x9b, 0xaf, 0xfe, 0xcd, 0x4c, 0x47, 0x79, 0xfb, 0xf5, 0x8f, 0x67, + 0x93, 0x40, 0x4c, 0x17, 0x57, 0xa7, 0xe3, 0x68, 0x7e, 0x36, 0x0b, 0x26, 0x53, 0x11, 0x06, 0xe1, + 0x24, 0xe4, 0xe2, 0xd7, 0x28, 0xb9, 0x39, 0x9b, 0x85, 0xfe, 0x99, 0x7a, 0xe3, 0xce, 0x96, 0xee, + 0xae, 0x8a, 0xea, 0xef, 0xd1, 0x37, 0xff, 0x04, 0x00, 0x00, 0xff, 0xff, 0x3c, 0xfc, 0x5a, 0x02, + 0x4e, 0x09, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -815,6 +882,10 @@ type RouterClient interface { //differs from SendPayment in that it allows users to specify a full route //manually. This can be used for things like rebalancing, and atomic swaps. SendToRoute(ctx context.Context, in *SendToRouteRequest, opts ...grpc.CallOption) (*SendToRouteResponse, error) + //* + //ResetMissionControl clears all mission control state and starts with a clean + //slate. + ResetMissionControl(ctx context.Context, in *ResetMissionControlRequest, opts ...grpc.CallOption) (*ResetMissionControlResponse, error) } type routerClient struct { @@ -852,6 +923,15 @@ func (c *routerClient) SendToRoute(ctx context.Context, in *SendToRouteRequest, return out, nil } +func (c *routerClient) ResetMissionControl(ctx context.Context, in *ResetMissionControlRequest, opts ...grpc.CallOption) (*ResetMissionControlResponse, error) { + out := new(ResetMissionControlResponse) + err := c.cc.Invoke(ctx, "/routerrpc.Router/ResetMissionControl", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // RouterServer is the server API for Router service. type RouterServer interface { //* @@ -870,6 +950,10 @@ type RouterServer interface { //differs from SendPayment in that it allows users to specify a full route //manually. This can be used for things like rebalancing, and atomic swaps. SendToRoute(context.Context, *SendToRouteRequest) (*SendToRouteResponse, error) + //* + //ResetMissionControl clears all mission control state and starts with a clean + //slate. + ResetMissionControl(context.Context, *ResetMissionControlRequest) (*ResetMissionControlResponse, error) } func RegisterRouterServer(s *grpc.Server, srv RouterServer) { @@ -930,6 +1014,24 @@ func _Router_SendToRoute_Handler(srv interface{}, ctx context.Context, dec func( return interceptor(ctx, in, info, handler) } +func _Router_ResetMissionControl_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ResetMissionControlRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RouterServer).ResetMissionControl(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/routerrpc.Router/ResetMissionControl", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RouterServer).ResetMissionControl(ctx, req.(*ResetMissionControlRequest)) + } + return interceptor(ctx, in, info, handler) +} + var _Router_serviceDesc = grpc.ServiceDesc{ ServiceName: "routerrpc.Router", HandlerType: (*RouterServer)(nil), @@ -946,6 +1048,10 @@ var _Router_serviceDesc = grpc.ServiceDesc{ MethodName: "SendToRoute", Handler: _Router_SendToRoute_Handler, }, + { + MethodName: "ResetMissionControl", + Handler: _Router_ResetMissionControl_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "routerrpc/router.proto", diff --git a/lnrpc/routerrpc/router.proto b/lnrpc/routerrpc/router.proto index 949922695731..f9262e81e7fe 100644 --- a/lnrpc/routerrpc/router.proto +++ b/lnrpc/routerrpc/router.proto @@ -209,6 +209,9 @@ message ChannelUpdate { // satoshi. uint32 fee_rate = 9; } +message ResetMissionControlRequest{} + +message ResetMissionControlResponse{} service Router { /** @@ -232,4 +235,10 @@ service Router { manually. This can be used for things like rebalancing, and atomic swaps. */ rpc SendToRoute(SendToRouteRequest) returns (SendToRouteResponse); + + /** + ResetMissionControl clears all mission control state and starts with a clean + slate. + */ + rpc ResetMissionControl(ResetMissionControlRequest) returns (ResetMissionControlResponse); } diff --git a/lnrpc/routerrpc/router_backend.go b/lnrpc/routerrpc/router_backend.go index 2b66f1f7ca27..97d4d88bd0d9 100644 --- a/lnrpc/routerrpc/router_backend.go +++ b/lnrpc/routerrpc/router_backend.go @@ -36,6 +36,8 @@ type RouterBackend struct { FindRoute func(source, target route.Vertex, amt lnwire.MilliSatoshi, restrictions *routing.RestrictParams, finalExpiry ...uint16) (*route.Route, error) + + MissionControl *routing.MissionControl } // QueryRoutes attempts to query the daemons' Channel Router for a possible diff --git a/lnrpc/routerrpc/router_server.go b/lnrpc/routerrpc/router_server.go index 1f63cefd06ce..0cec2c8faee2 100644 --- a/lnrpc/routerrpc/router_server.go +++ b/lnrpc/routerrpc/router_server.go @@ -59,6 +59,10 @@ var ( Entity: "offchain", Action: "read", }}, + "/routerrpc.Router/ResetMissionControl": {{ + Entity: "offchain", + Action: "write", + }}, } // DefaultRouterMacFilename is the default name of the router macaroon @@ -436,3 +440,13 @@ func marshallChannelUpdate(update *lnwire.ChannelUpdate) *ChannelUpdate { FeeRate: update.FeeRate, } } + +// ResetMissionControl clears all mission control state and starts with a clean +// slate. +func (s *Server) ResetMissionControl(ctx context.Context, + req *ResetMissionControlRequest) (*ResetMissionControlResponse, error) { + + s.cfg.RouterBackend.MissionControl.ResetHistory() + + return &ResetMissionControlResponse{}, nil +} diff --git a/routing/router.go b/routing/router.go index 582e1401389d..5588dfe897f1 100644 --- a/routing/router.go +++ b/routing/router.go @@ -199,6 +199,15 @@ type Config struct { // their results. Payer PaymentAttemptDispatcher + // MissionControl is a shared memory of sorts that executions of + // payment path finding use in order to remember which vertexes/edges + // were pruned from prior attempts. During SendPayment execution, + // errors sent by nodes are mapped into a vertex or edge to be pruned. + // Each run will then take into account this set of pruned + // vertexes/edges to reduce route failure and pass on graph information + // gained to the next execution. + MissionControl *MissionControl + // ChannelPruneExpiry is the duration used to determine if a channel // should be pruned or not. If the delta between now and when the // channel was last updated is greater than ChannelPruneExpiry, then @@ -338,15 +347,6 @@ type ChannelRouter struct { // existing client. ntfnClientUpdates chan *topologyClientUpdate - // missionControl is a shared memory of sorts that executions of - // payment path finding use in order to remember which vertexes/edges - // were pruned from prior attempts. During SendPayment execution, - // errors sent by nodes are mapped into a vertex or edge to be pruned. - // Each run will then take into account this set of pruned - // vertexes/edges to reduce route failure and pass on graph information - // gained to the next execution. - missionControl *MissionControl - // channelEdgeMtx is a mutex we use to make sure we process only one // ChannelEdgePolicy at a time for a given channelID, to ensure // consistency between the various database accesses. @@ -388,10 +388,6 @@ func New(cfg Config) (*ChannelRouter, error) { quit: make(chan struct{}), } - r.missionControl = NewMissionControl( - cfg.Graph, selfNode, cfg.QueryBandwidth, - ) - return r, nil } @@ -1539,11 +1535,13 @@ type LightningPayment struct { // will be returned which describes the path the successful payment traversed // within the network to reach the destination. Additionally, the payment // preimage will also be returned. -func (r *ChannelRouter) SendPayment(payment *LightningPayment) ([32]byte, *route.Route, error) { +func (r *ChannelRouter) SendPayment(payment *LightningPayment) ([32]byte, + *route.Route, error) { + // Before starting the HTLC routing attempt, we'll create a fresh // payment session which will report our errors back to mission // control. - paySession, err := r.missionControl.newPaymentSession( + paySession, err := r.cfg.MissionControl.newPaymentSession( payment.RouteHints, payment.Target, ) if err != nil { @@ -1560,7 +1558,7 @@ func (r *ChannelRouter) SendToRoute(hash lntypes.Hash, route *route.Route) ( lntypes.Preimage, error) { // Create a payment session for just this route. - paySession := r.missionControl.newPaymentSessionForRoute(route) + paySession := r.cfg.MissionControl.newPaymentSessionForRoute(route) // Create a (mostly) dummy payment, as the created payment session is // not going to do path finding. diff --git a/routing/router_test.go b/routing/router_test.go index 9dc0eed19c98..900686d02f10 100644 --- a/routing/router_test.go +++ b/routing/router_test.go @@ -83,6 +83,16 @@ func createTestCtxFromGraphInstance(startingHeight uint32, graphInstance *testGr // be populated. chain := newMockChain(startingHeight) chainView := newMockChainView(chain) + + selfNode, err := graphInstance.graph.SourceNode() + if err != nil { + return nil, nil, err + } + + queryBandwidth := func(e *channeldb.ChannelEdgeInfo) lnwire.MilliSatoshi { + return lnwire.NewMSatFromSatoshis(e.Capacity) + } + router, err := New(Config{ Graph: graphInstance.graph, Chain: chain, @@ -97,6 +107,9 @@ func createTestCtxFromGraphInstance(startingHeight uint32, graphInstance *testGr next := atomic.AddUint64(&uniquePaymentID, 1) return next, nil }, + MissionControl: NewMissionControl( + graphInstance.graph, selfNode, queryBandwidth, + ), }) if err != nil { return nil, nil, fmt.Errorf("unable to create router %v", err) @@ -805,7 +818,7 @@ func TestSendPaymentErrorPathPruning(t *testing.T) { return preImage, nil }) - ctx.router.missionControl.ResetHistory() + ctx.router.cfg.MissionControl.ResetHistory() // When we try to dispatch that payment, we should receive an error as // both attempts should fail and cause both routes to be pruned. @@ -820,7 +833,7 @@ func TestSendPaymentErrorPathPruning(t *testing.T) { t.Fatalf("expected UnknownNextPeer instead got: %v", err) } - ctx.router.missionControl.ResetHistory() + ctx.router.cfg.MissionControl.ResetHistory() // Next, we'll modify the SendToSwitch method to indicate that luo ji // wasn't originally online. This should also halt the send all @@ -863,7 +876,7 @@ func TestSendPaymentErrorPathPruning(t *testing.T) { ctx.aliases)) } - ctx.router.missionControl.ResetHistory() + ctx.router.cfg.MissionControl.ResetHistory() // Finally, we'll modify the SendToSwitch function to indicate that the // roasbeef -> luoji channel has insufficient capacity. This should diff --git a/rpcserver.go b/rpcserver.go index a98845dfcedc..3f6fd078fb24 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -477,7 +477,8 @@ func newRPCServer(s *server, macService *macaroons.Service, return info.NodeKey1Bytes, info.NodeKey2Bytes, nil }, - FindRoute: s.chanRouter.FindRoute, + FindRoute: s.chanRouter.FindRoute, + MissionControl: s.missionControl, } var ( diff --git a/server.go b/server.go index cc2033ad6798..68daf7ae377f 100644 --- a/server.go +++ b/server.go @@ -187,6 +187,8 @@ type server struct { breachArbiter *breachArbiter + missionControl *routing.MissionControl + chanRouter *routing.ChannelRouter authGossiper *discovery.AuthenticatedGossiper @@ -609,6 +611,40 @@ func newServer(listenAddrs []net.Addr, chanDB *channeldb.DB, cc *chainControl, } s.currentNodeAnn = nodeAnn + queryBandwidth := func(edge *channeldb.ChannelEdgeInfo) lnwire.MilliSatoshi { + // If we aren't on either side of this edge, then we'll + // just thread through the capacity of the edge as we + // know it. + if !bytes.Equal(edge.NodeKey1Bytes[:], selfNode.PubKeyBytes[:]) && + !bytes.Equal(edge.NodeKey2Bytes[:], selfNode.PubKeyBytes[:]) { + + return lnwire.NewMSatFromSatoshis(edge.Capacity) + } + + cid := lnwire.NewChanIDFromOutPoint(&edge.ChannelPoint) + link, err := s.htlcSwitch.GetLink(cid) + if err != nil { + // If the link isn't online, then we'll report + // that it has zero bandwidth to the router. + return 0 + } + + // If the link is found within the switch, but it isn't + // yet eligible to forward any HTLCs, then we'll treat + // it as if it isn't online in the first place. + if !link.EligibleToForward() { + return 0 + } + + // Otherwise, we'll return the current best estimate + // for the available bandwidth for the link. + return link.Bandwidth() + } + + s.missionControl = routing.NewMissionControl( + chanGraph, selfNode, queryBandwidth, + ) + // The router will get access to the payment ID sequencer, such that it // can generate unique payment IDs. sequencer, err := htlcswitch.NewPersistentSequencer(chanDB) @@ -621,37 +657,10 @@ func newServer(listenAddrs []net.Addr, chanDB *channeldb.DB, cc *chainControl, Chain: cc.chainIO, ChainView: cc.chainView, Payer: s.htlcSwitch, + MissionControl: s.missionControl, ChannelPruneExpiry: routing.DefaultChannelPruneExpiry, GraphPruneInterval: time.Duration(time.Hour), - QueryBandwidth: func(edge *channeldb.ChannelEdgeInfo) lnwire.MilliSatoshi { - // If we aren't on either side of this edge, then we'll - // just thread through the capacity of the edge as we - // know it. - if !bytes.Equal(edge.NodeKey1Bytes[:], selfNode.PubKeyBytes[:]) && - !bytes.Equal(edge.NodeKey2Bytes[:], selfNode.PubKeyBytes[:]) { - - return lnwire.NewMSatFromSatoshis(edge.Capacity) - } - - cid := lnwire.NewChanIDFromOutPoint(&edge.ChannelPoint) - link, err := s.htlcSwitch.GetLink(cid) - if err != nil { - // If the link isn't online, then we'll report - // that it has zero bandwidth to the router. - return 0 - } - - // If the link is found within the switch, but it isn't - // yet eligible to forward any HTLCs, then we'll treat - // it as if it isn't online in the first place. - if !link.EligibleToForward() { - return 0 - } - - // Otherwise, we'll return the current best estimate - // for the available bandwidth for the link. - return link.Bandwidth() - }, + QueryBandwidth: queryBandwidth, AssumeChannelValid: cfg.Routing.UseAssumeChannelValid(), NextPaymentID: sequencer.NextID, }) From 34f08a2b1808812ae72b30fa85d78382bdf925d9 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Tue, 21 May 2019 10:04:57 +0200 Subject: [PATCH 05/11] routing: remove querybandwidth self node check This function is only ever called for channels connected to self. --- server.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/server.go b/server.go index 68daf7ae377f..c35cfdf24927 100644 --- a/server.go +++ b/server.go @@ -612,15 +612,6 @@ func newServer(listenAddrs []net.Addr, chanDB *channeldb.DB, cc *chainControl, s.currentNodeAnn = nodeAnn queryBandwidth := func(edge *channeldb.ChannelEdgeInfo) lnwire.MilliSatoshi { - // If we aren't on either side of this edge, then we'll - // just thread through the capacity of the edge as we - // know it. - if !bytes.Equal(edge.NodeKey1Bytes[:], selfNode.PubKeyBytes[:]) && - !bytes.Equal(edge.NodeKey2Bytes[:], selfNode.PubKeyBytes[:]) { - - return lnwire.NewMSatFromSatoshis(edge.Capacity) - } - cid := lnwire.NewChanIDFromOutPoint(&edge.ChannelPoint) link, err := s.htlcSwitch.GetLink(cid) if err != nil { From ed335eb5a22ab511ba1b6da4ac1c153a20cc815d Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Tue, 19 Mar 2019 11:45:10 +0100 Subject: [PATCH 06/11] routing: use probability source in path finding This PR replaces the previously used edge and node ignore lists in path finding by a probability based system. It modifies path finding so that it not only compares routes on fee and time lock, but also takes route success probability into account. Allowing routes to be compared based on success probability is achieved by introducing a 'virtual' cost of a payment attempt and using that to translate probability into another cost factor. --- lnrpc/routerrpc/router_backend.go | 18 +- lnrpc/routerrpc/router_backend_test.go | 29 ++-- routing/heap.go | 10 +- routing/pathfind.go | 133 +++++++++++---- routing/pathfind_test.go | 218 ++++++++++++++++++------- routing/payment_session.go | 24 ++- routing/router_test.go | 7 +- 7 files changed, 322 insertions(+), 117 deletions(-) diff --git a/lnrpc/routerrpc/router_backend.go b/lnrpc/routerrpc/router_backend.go index 97d4d88bd0d9..dfda2109af6d 100644 --- a/lnrpc/routerrpc/router_backend.go +++ b/lnrpc/routerrpc/router_backend.go @@ -123,9 +123,21 @@ func (r *RouterBackend) QueryRoutes(ctx context.Context, } restrictions := &routing.RestrictParams{ - FeeLimit: feeLimit, - IgnoredNodes: ignoredNodes, - IgnoredEdges: ignoredEdges, + FeeLimit: feeLimit, + ProbabilitySource: func(node route.Vertex, + edge routing.EdgeLocator) float64 { + + if _, ok := ignoredNodes[node]; ok { + return 0 + } + + if _, ok := ignoredEdges[edge]; ok { + return 0 + } + + return 1 + }, + PaymentAttemptPenalty: routing.DefaultPaymentAttemptPenalty, } // Query the channel router for a possible path to the destination that diff --git a/lnrpc/routerrpc/router_backend_test.go b/lnrpc/routerrpc/router_backend_test.go index 452ebf261828..f0913359c29d 100644 --- a/lnrpc/routerrpc/router_backend_test.go +++ b/lnrpc/routerrpc/router_backend_test.go @@ -39,6 +39,11 @@ func TestQueryRoutes(t *testing.T) { t.Fatal(err) } + ignoredEdge := routing.EdgeLocator{ + ChannelID: 555, + Direction: 1, + } + request := &lnrpc.QueryRoutesRequest{ PubKey: destKey, Amt: 100000, @@ -75,22 +80,22 @@ func TestQueryRoutes(t *testing.T) { t.Fatal("unexpected fee limit") } - if len(restrictions.IgnoredEdges) != 1 { - t.Fatal("unexpected ignored edges map size") - } - - if _, ok := restrictions.IgnoredEdges[routing.EdgeLocator{ - ChannelID: 555, Direction: 1, - }]; !ok { - t.Fatal("unexpected ignored edge") + if restrictions.ProbabilitySource(route.Vertex{}, + ignoredEdge, + ) != 0 { + t.Fatal("expecting 0% probability for ignored edge") } - if len(restrictions.IgnoredNodes) != 1 { - t.Fatal("unexpected ignored nodes map size") + if restrictions.ProbabilitySource(ignoreNodeVertex, + routing.EdgeLocator{}, + ) != 0 { + t.Fatal("expecting 0% probability for ignored node") } - if _, ok := restrictions.IgnoredNodes[ignoreNodeVertex]; !ok { - t.Fatal("unexpected ignored node") + if restrictions.ProbabilitySource(route.Vertex{}, + routing.EdgeLocator{}, + ) != 1 { + t.Fatal("expecting 100% probability") } hops := []*route.Hop{&route.Hop{}} diff --git a/routing/heap.go b/routing/heap.go index 80336fd0ce00..be7acaf0adb2 100644 --- a/routing/heap.go +++ b/routing/heap.go @@ -25,8 +25,14 @@ type nodeWithDist struct { // node. This value does not include the final cltv. incomingCltv uint32 - // fee is the fee that this node is charging for forwarding. - fee lnwire.MilliSatoshi + // probability is the probability that from this node onward the route + // is successful. + probability float64 + + // weight is the cost of the route from this node to the destination. + // Includes the routing fees and a virtual cost factor to account for + // time locks. + weight int64 } // distanceHeap is a min-distance heap that's used within our path finding diff --git a/routing/pathfind.go b/routing/pathfind.go index 222479854724..efa4c99b81f5 100644 --- a/routing/pathfind.go +++ b/routing/pathfind.go @@ -40,6 +40,14 @@ type pathFinder = func(g *graphParams, r *RestrictParams, source, target route.Vertex, amt lnwire.MilliSatoshi) ( []*channeldb.ChannelEdgePolicy, error) +var ( + // DefaultPaymentAttemptPenalty is the virtual cost in path finding weight + // units of executing a payment attempt that fails. It is used to trade + // off potentially better routes against their probability of + // succeeding. + DefaultPaymentAttemptPenalty = lnwire.NewMSatFromSatoshis(100) +) + // edgePolicyWithSource is a helper struct to keep track of the source node // of a channel edge. ChannelEdgePolicy only contains to destination node // of the edge. @@ -228,13 +236,9 @@ type graphParams struct { // RestrictParams wraps the set of restrictions passed to findPath that the // found path must adhere to. type RestrictParams struct { - // IgnoredNodes is an optional set of nodes that should be ignored if - // encountered during path finding. - IgnoredNodes map[route.Vertex]struct{} - - // IgnoredEdges is an optional set of edges that should be ignored if - // encountered during path finding. - IgnoredEdges map[EdgeLocator]struct{} + // ProbabilitySource is a callback that is expected to return the + // success probability of traversing the channel from the node. + ProbabilitySource func(route.Vertex, EdgeLocator) float64 // FeeLimit is a maximum fee amount allowed to be used on the path from // the source to the target. @@ -248,6 +252,12 @@ type RestrictParams struct { // ctlv. After path finding is complete, the caller needs to increase // all cltv expiry heights with the required final cltv delta. CltvLimit *uint32 + + // PaymentAttemptPenalty is the virtual cost in path finding weight + // units of executing a payment attempt that fails. It is used to trade + // off potentially better routes against their probability of + // succeeding. + PaymentAttemptPenalty lnwire.MilliSatoshi } // findPath attempts to find a path from the source node within the @@ -331,10 +341,11 @@ func findPath(g *graphParams, r *RestrictParams, source, target route.Vertex, targetNode := &channeldb.LightningNode{PubKeyBytes: target} distance[target] = nodeWithDist{ dist: 0, + weight: 0, node: targetNode, amountToReceive: amt, - fee: 0, incomingCltv: 0, + probability: 1, } // We'll use this map as a series of "next" hop pointers. So to get @@ -342,15 +353,6 @@ func findPath(g *graphParams, r *RestrictParams, source, target route.Vertex, // mapped to within `next`. next := make(map[route.Vertex]*channeldb.ChannelEdgePolicy) - ignoredEdges := r.IgnoredEdges - if ignoredEdges == nil { - ignoredEdges = make(map[EdgeLocator]struct{}) - } - ignoredNodes := r.IgnoredNodes - if ignoredNodes == nil { - ignoredNodes = make(map[route.Vertex]struct{}) - } - // processEdge is a helper closure that will be used to make sure edges // satisfy our specific requirements. processEdge := func(fromNode *channeldb.LightningNode, @@ -380,21 +382,22 @@ func findPath(g *graphParams, r *RestrictParams, source, target route.Vertex, return } - // If this vertex or edge has been black listed, then we'll - // skip exploring this edge. - if _, ok := ignoredNodes[fromVertex]; ok { - return - } + // Calculate amount that the candidate node would have to sent + // out. + toNodeDist := distance[toNode] + amountToSend := toNodeDist.amountToReceive + // Request the success probability for this edge. locator := newEdgeLocator(edge) - if _, ok := ignoredEdges[*locator]; ok { + edgeProbability := r.ProbabilitySource( + fromVertex, *locator, + ) + + // If the probability is zero, there is no point in trying. + if edgeProbability == 0 { return } - toNodeDist := distance[toNode] - - amountToSend := toNodeDist.amountToReceive - // If the estimated bandwidth of the channel edge is not able // to carry the amount that needs to be send, return. if bandwidth < amountToSend { @@ -453,20 +456,35 @@ func findPath(g *graphParams, r *RestrictParams, source, target route.Vertex, return } + // Calculate total probability of successfully reaching target + // by multiplying the probabilities. Both this edge and the rest + // of the route must succeed. + probability := toNodeDist.probability * edgeProbability + // By adding fromNode in the route, there will be an extra // weight composed of the fee that this node will charge and // the amount that will be locked for timeLockDelta blocks in // the HTLC that is handed out to fromNode. weight := edgeWeight(amountToReceive, fee, timeLockDelta) - // Compute the tentative distance to this new channel/edge - // which is the distance from our toNode to the target node + // Compute the tentative weight to this new channel/edge + // which is the weight from our toNode to the target node // plus the weight of this edge. - tempDist := toNodeDist.dist + weight + tempWeight := toNodeDist.weight + weight + + // Add an extra factor to the weight to take into account the + // probability. + tempDist := getProbabilityBasedDist( + tempWeight, probability, int64(r.PaymentAttemptPenalty), + ) + + // If the current best route is better than this candidate + // route, return. It is important to also return if the distance + // is equal, because otherwise the algorithm could run into an + // endless loop. + if distance[fromVertex].dist != infinity && + tempDist >= distance[fromVertex].dist { - // If this new tentative distance is not better than the current - // best known distance to this node, return. - if tempDist >= distance[fromVertex].dist { return } @@ -483,10 +501,11 @@ func findPath(g *graphParams, r *RestrictParams, source, target route.Vertex, // map is populated with this edge. distance[fromVertex] = nodeWithDist{ dist: tempDist, + weight: tempWeight, node: fromNode, amountToReceive: amountToReceive, - fee: fee, incomingCltv: incomingCltv, + probability: probability, } next[fromVertex] = edge @@ -614,5 +633,51 @@ func findPath(g *graphParams, r *RestrictParams, source, target route.Vertex, "too many hops") } + log.Infof("Found route with probability: %v\n", distance[source].probability) + return pathEdges, nil } + +// getProbabilityBasedDist converts a weight into a distance that takes into +// account the success probability and the (virtual) cost of a failed payment +// attempt. +// +// Derivation: +// +// Suppose there are two routes A and B with fees Fa and Fb and success +// probabilities Pa and Pb. +// +// Is the expected cost of trying route A first and then B lower than trying the +// other way around? +// +// The expected cost of A-then-B is: Pa*Fa + (1-Pa)*Pb*(c+Fb) +// +// The expected cost of B-then-A is: Pb*Fb + (1-Pb)*Pa*(c+Fa) +// +// In these equations, the term representing the case where both A and B fail is +// left out because its value would be the same in both cases. +// +// Pa*Fa + (1-Pa)*Pb*(c+Fb) < Pb*Fb + (1-Pb)*Pa*(c+Fa) +// +// Pa*Fa + Pb*c + Pb*Fb - Pa*Pb*c - Pa*Pb*Fb < Pb*Fb + Pa*c + Pa*Fa - Pa*Pb*c - Pa*Pb*Fa +// +// Removing terms that cancel out: +// Pb*c - Pa*Pb*Fb < Pa*c - Pa*Pb*Fa +// +// Divide by Pa*Pb: +// c/Pa - Fb < c/Pb - Fa +// +// Move terms around: +// Fa + c/Pa < Fb + c/Pb +// +// So the value of F + c/P can be used to compare routes. +func getProbabilityBasedDist(weight int64, probability float64, penalty int64) int64 { + // Clamp probability to prevent overflow. + const minProbability = 0.00001 + + if probability < minProbability { + return infinity + } + + return weight + int64(float64(penalty)/probability) +} diff --git a/routing/pathfind_test.go b/routing/pathfind_test.go index fb5ca0a57bca..280dd64a3a2a 100644 --- a/routing/pathfind_test.go +++ b/routing/pathfind_test.go @@ -52,7 +52,8 @@ const ( var ( noRestrictions = &RestrictParams{ - FeeLimit: noFeeLimit, + FeeLimit: noFeeLimit, + ProbabilitySource: noProbabilitySource, } ) @@ -72,6 +73,12 @@ var ( } ) +// noProbabilitySource is used in testing to return the same probability 1 for +// all edges. +func noProbabilitySource(route.Vertex, EdgeLocator) float64 { + return 1 +} + // testGraph is the struct which corresponds to the JSON format used to encode // graphs within the files in the testdata directory. // @@ -635,9 +642,7 @@ func TestFindLowestFeePath(t *testing.T) { &graphParams{ graph: testGraphInstance.graph, }, - &RestrictParams{ - FeeLimit: noFeeLimit, - }, + noRestrictions, sourceNode.PubKeyBytes, target, paymentAmt, ) if err != nil { @@ -776,7 +781,8 @@ func testBasicGraphPathFindingCase(t *testing.T, graphInstance *testGraphInstanc graph: graphInstance.graph, }, &RestrictParams{ - FeeLimit: test.feeLimit, + FeeLimit: test.feeLimit, + ProbabilitySource: noProbabilitySource, }, sourceNode.PubKeyBytes, target, paymentAmt, ) @@ -944,9 +950,7 @@ func TestPathFindingWithAdditionalEdges(t *testing.T) { graph: graph.graph, additionalEdges: additionalEdges, }, - &RestrictParams{ - FeeLimit: noFeeLimit, - }, + noRestrictions, sourceNode.PubKeyBytes, doge.PubKeyBytes, paymentAmt, ) if err != nil { @@ -1200,9 +1204,7 @@ func TestNewRoutePathTooLong(t *testing.T) { &graphParams{ graph: graph.graph, }, - &RestrictParams{ - FeeLimit: noFeeLimit, - }, + noRestrictions, sourceNode.PubKeyBytes, target, paymentAmt, ) if err != nil { @@ -1216,9 +1218,7 @@ func TestNewRoutePathTooLong(t *testing.T) { &graphParams{ graph: graph.graph, }, - &RestrictParams{ - FeeLimit: noFeeLimit, - }, + noRestrictions, sourceNode.PubKeyBytes, target, paymentAmt, ) if err == nil { @@ -1258,9 +1258,7 @@ func TestPathNotAvailable(t *testing.T) { &graphParams{ graph: graph.graph, }, - &RestrictParams{ - FeeLimit: noFeeLimit, - }, + noRestrictions, sourceNode.PubKeyBytes, unknownNode, 100, ) if !IsError(err, ErrNoPathFound) { @@ -1297,9 +1295,7 @@ func TestPathInsufficientCapacity(t *testing.T) { &graphParams{ graph: graph.graph, }, - &RestrictParams{ - FeeLimit: noFeeLimit, - }, + noRestrictions, sourceNode.PubKeyBytes, target, payAmt, ) if !IsError(err, ErrNoPathFound) { @@ -1332,9 +1328,7 @@ func TestRouteFailMinHTLC(t *testing.T) { &graphParams{ graph: graph.graph, }, - &RestrictParams{ - FeeLimit: noFeeLimit, - }, + noRestrictions, sourceNode.PubKeyBytes, target, payAmt, ) if !IsError(err, ErrNoPathFound) { @@ -1392,9 +1386,7 @@ func TestRouteFailMaxHTLC(t *testing.T) { &graphParams{ graph: graph.graph, }, - &RestrictParams{ - FeeLimit: noFeeLimit, - }, + noRestrictions, sourceNode.PubKeyBytes, target, payAmt, ) if err != nil { @@ -1416,9 +1408,7 @@ func TestRouteFailMaxHTLC(t *testing.T) { &graphParams{ graph: graph.graph, }, - &RestrictParams{ - FeeLimit: noFeeLimit, - }, + noRestrictions, sourceNode.PubKeyBytes, target, payAmt, ) if !IsError(err, ErrNoPathFound) { @@ -1453,9 +1443,7 @@ func TestRouteFailDisabledEdge(t *testing.T) { &graphParams{ graph: graph.graph, }, - &RestrictParams{ - FeeLimit: noFeeLimit, - }, + noRestrictions, sourceNode.PubKeyBytes, target, payAmt, ) if err != nil { @@ -1483,9 +1471,7 @@ func TestRouteFailDisabledEdge(t *testing.T) { &graphParams{ graph: graph.graph, }, - &RestrictParams{ - FeeLimit: noFeeLimit, - }, + noRestrictions, sourceNode.PubKeyBytes, target, payAmt, ) if err != nil { @@ -1510,9 +1496,7 @@ func TestRouteFailDisabledEdge(t *testing.T) { &graphParams{ graph: graph.graph, }, - &RestrictParams{ - FeeLimit: noFeeLimit, - }, + noRestrictions, sourceNode.PubKeyBytes, target, payAmt, ) if !IsError(err, ErrNoPathFound) { @@ -1546,9 +1530,7 @@ func TestPathSourceEdgesBandwidth(t *testing.T) { &graphParams{ graph: graph.graph, }, - &RestrictParams{ - FeeLimit: noFeeLimit, - }, + noRestrictions, sourceNode.PubKeyBytes, target, payAmt, ) if err != nil { @@ -1572,9 +1554,7 @@ func TestPathSourceEdgesBandwidth(t *testing.T) { graph: graph.graph, bandwidthHints: bandwidths, }, - &RestrictParams{ - FeeLimit: noFeeLimit, - }, + noRestrictions, sourceNode.PubKeyBytes, target, payAmt, ) if !IsError(err, ErrNoPathFound) { @@ -1592,9 +1572,7 @@ func TestPathSourceEdgesBandwidth(t *testing.T) { graph: graph.graph, bandwidthHints: bandwidths, }, - &RestrictParams{ - FeeLimit: noFeeLimit, - }, + noRestrictions, sourceNode.PubKeyBytes, target, payAmt, ) if err != nil { @@ -1625,9 +1603,7 @@ func TestPathSourceEdgesBandwidth(t *testing.T) { graph: graph.graph, bandwidthHints: bandwidths, }, - &RestrictParams{ - FeeLimit: noFeeLimit, - }, + noRestrictions, sourceNode.PubKeyBytes, target, payAmt, ) if err != nil { @@ -1916,6 +1892,7 @@ func TestRestrictOutgoingChannel(t *testing.T) { &RestrictParams{ FeeLimit: noFeeLimit, OutgoingChannelID: &outgoingChannelID, + ProbabilitySource: noProbabilitySource, }, sourceVertex, target, paymentAmt, ) @@ -1992,9 +1969,6 @@ func testCltvLimit(t *testing.T, limit uint32, expectedChannel uint64) { } sourceVertex := route.Vertex(sourceNode.PubKeyBytes) - ignoredEdges := make(map[EdgeLocator]struct{}) - ignoredVertexes := make(map[route.Vertex]struct{}) - paymentAmt := lnwire.NewMSatFromSatoshis(100) target := testGraphInstance.aliasMap["target"] @@ -2009,10 +1983,9 @@ func testCltvLimit(t *testing.T, limit uint32, expectedChannel uint64) { graph: testGraphInstance.graph, }, &RestrictParams{ - IgnoredNodes: ignoredVertexes, - IgnoredEdges: ignoredEdges, - FeeLimit: noFeeLimit, - CltvLimit: cltvLimit, + FeeLimit: noFeeLimit, + CltvLimit: cltvLimit, + ProbabilitySource: noProbabilitySource, }, sourceVertex, target, paymentAmt, ) @@ -2045,3 +2018,134 @@ func testCltvLimit(t *testing.T, limit uint32, expectedChannel uint64) { route.Hops[0].ChannelID) } } + +// TestProbabilityRouting asserts that path finding not only takes into account +// fees but also success probability. +func TestProbabilityRouting(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + p10, p11, p20 float64 + expectedChan uint64 + }{ + // Test two variations with probabilities that should multiply + // to the same total route probability. In both cases the three + // hop route should be the best route. The three hop route has a + // probability of 0.5 * 0.8 = 0.4. The fee is 5 (chan 10) + 8 + // (chan 11) = 13. Path finding distance should work out to: 13 + // + 10 (attempt penalty) / 0.4 = 38. The two hop route is 25 + + // 10 / 0.7 = 39. + { + name: "three hop 1", + p10: 0.8, p11: 0.5, p20: 0.7, + expectedChan: 10, + }, + { + name: "three hop 2", + p10: 0.5, p11: 0.8, p20: 0.7, + expectedChan: 10, + }, + + // If the probability of the two hop route is increased, its + // distance becomes 25 + 10 / 0.85 = 37. This is less than the + // three hop route with its distance 38. So with an attempt + // penalty of 10, the higher fee route is chosen because of the + // compensation for success probability. + { + name: "two hop higher cost", + p10: 0.5, p11: 0.8, p20: 0.85, + expectedChan: 20, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testProbabilityRouting( + t, tc.p10, tc.p11, tc.p20, tc.expectedChan, + ) + }) + } +} + +func testProbabilityRouting(t *testing.T, p10, p11, p20 float64, + expectedChan uint64) { + + t.Parallel() + + // Set up a test graph with two possible paths to the target: a three + // hop path (via channels 10 and 11) and a two hop path (via channel + // 20). + testChannels := []*testChannel{ + symmetricTestChannel("roasbeef", "a1", 100000, &testChannelPolicy{}), + symmetricTestChannel("roasbeef", "b", 100000, &testChannelPolicy{}), + symmetricTestChannel("roasbeef", "c", 100000, &testChannelPolicy{}), + symmetricTestChannel("a1", "a2", 100000, &testChannelPolicy{ + Expiry: 144, + FeeBaseMsat: lnwire.NewMSatFromSatoshis(5), + MinHTLC: 1, + }, 10), + symmetricTestChannel("a2", "target", 100000, &testChannelPolicy{ + Expiry: 144, + FeeBaseMsat: lnwire.NewMSatFromSatoshis(8), + MinHTLC: 1, + }, 11), + symmetricTestChannel("b", "target", 100000, &testChannelPolicy{ + Expiry: 100, + FeeBaseMsat: lnwire.NewMSatFromSatoshis(25), + MinHTLC: 1, + }, 20), + } + + testGraphInstance, err := createTestGraphFromChannels(testChannels) + if err != nil { + t.Fatalf("unable to create graph: %v", err) + } + defer testGraphInstance.cleanUp() + + sourceNode, err := testGraphInstance.graph.SourceNode() + if err != nil { + t.Fatalf("unable to fetch source node: %v", err) + } + sourceVertex := route.Vertex(sourceNode.PubKeyBytes) + + paymentAmt := lnwire.NewMSatFromSatoshis(100) + target := testGraphInstance.aliasMap["target"] + + // Configure a probability source with the test parameters. + probabilitySource := func(node route.Vertex, edge EdgeLocator) float64 { + + switch edge.ChannelID { + case 10: + return p10 + case 11: + return p11 + case 20: + return p20 + default: + return 1 + } + } + + path, err := findPath( + &graphParams{ + graph: testGraphInstance.graph, + }, + &RestrictParams{ + FeeLimit: noFeeLimit, + ProbabilitySource: probabilitySource, + PaymentAttemptPenalty: lnwire.NewMSatFromSatoshis(10), + }, + sourceVertex, target, paymentAmt, + ) + if err != nil { + t.Fatal(err) + } + + // Assert that the route passes through the expected channel. + if path[1].ChannelID != expectedChan { + t.Fatalf("expected route to pass through channel %v, "+ + "but channel %v was selected instead", expectedChan, + path[1].ChannelID) + } +} diff --git a/routing/payment_session.go b/routing/payment_session.go index 4f9edba5d29e..bd4210fdc680 100644 --- a/routing/payment_session.go +++ b/routing/payment_session.go @@ -103,6 +103,20 @@ func (p *paymentSession) ReportEdgePolicyFailure( p.errFailedPolicyChans[*failedEdge] = struct{}{} } +func (p *paymentSession) getEdgeProbability(node route.Vertex, + edge EdgeLocator) float64 { + + if _, ok := p.pruneViewSnapshot.vertexes[node]; ok { + return 0 + } + + if _, ok := p.pruneViewSnapshot.edges[edge]; ok { + return 0 + } + + return 1 +} + // RequestRoute returns a route which is likely to be capable for successfully // routing the specified HTLC payment to the target node. Initially the first // set of paths returned from this method may encounter routing failure along @@ -159,11 +173,11 @@ func (p *paymentSession) RequestRoute(payment *LightningPayment, bandwidthHints: p.bandwidthHints, }, &RestrictParams{ - IgnoredNodes: pruneView.vertexes, - IgnoredEdges: pruneView.edges, - FeeLimit: payment.FeeLimit, - OutgoingChannelID: payment.OutgoingChannelID, - CltvLimit: cltvLimit, + ProbabilitySource: p.getEdgeProbability, + FeeLimit: payment.FeeLimit, + OutgoingChannelID: payment.OutgoingChannelID, + CltvLimit: cltvLimit, + PaymentAttemptPenalty: DefaultPaymentAttemptPenalty, }, p.mc.selfNode.PubKeyBytes, payment.Target, payment.Amount, diff --git a/routing/router_test.go b/routing/router_test.go index 900686d02f10..f478e4b3d418 100644 --- a/routing/router_test.go +++ b/routing/router_test.go @@ -199,7 +199,8 @@ func TestFindRoutesWithFeeLimit(t *testing.T) { target := ctx.aliases["sophon"] paymentAmt := lnwire.NewMSatFromSatoshis(100) restrictions := &RestrictParams{ - FeeLimit: lnwire.NewMSatFromSatoshis(10), + FeeLimit: lnwire.NewMSatFromSatoshis(10), + ProbabilitySource: noProbabilitySource, } route, err := ctx.router.FindRoute( @@ -2191,9 +2192,7 @@ func TestFindPathFeeWeighting(t *testing.T) { &graphParams{ graph: ctx.graph, }, - &RestrictParams{ - FeeLimit: noFeeLimit, - }, + noRestrictions, sourceNode.PubKeyBytes, target, amt, ) if err != nil { From e150dc642a101629c872cb2ee235f4136f87a44a Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Mon, 13 May 2019 17:00:35 +0200 Subject: [PATCH 07/11] routing: add min probability to path finding This commit adds a new restriction to pathfinding that allows returning only routes with a minimum success probability. --- routing/pathfind.go | 15 ++++++++++++ routing/pathfind_test.go | 49 ++++++++++++++++++++++++++++++++-------- 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/routing/pathfind.go b/routing/pathfind.go index efa4c99b81f5..fbd60682c64e 100644 --- a/routing/pathfind.go +++ b/routing/pathfind.go @@ -46,6 +46,10 @@ var ( // off potentially better routes against their probability of // succeeding. DefaultPaymentAttemptPenalty = lnwire.NewMSatFromSatoshis(100) + + // DefaultMinProbability is the default minimum probability for routes + // returned from findPath. + DefaultMinProbability = float64(0.01) ) // edgePolicyWithSource is a helper struct to keep track of the source node @@ -258,6 +262,10 @@ type RestrictParams struct { // off potentially better routes against their probability of // succeeding. PaymentAttemptPenalty lnwire.MilliSatoshi + + // MinProbability defines the minimum success probability of the + // returned route. + MinProbability float64 } // findPath attempts to find a path from the source node within the @@ -461,6 +469,13 @@ func findPath(g *graphParams, r *RestrictParams, source, target route.Vertex, // of the route must succeed. probability := toNodeDist.probability * edgeProbability + // If the probability is below the specified lower bound, we can + // abandon this direction. Adding further nodes can only lower + // the probability more. + if probability < r.MinProbability { + return + } + // By adding fromNode in the route, there will be an extra // weight composed of the fee that this node will charge and // the amount that will be locked for timeLockDelta blocks in diff --git a/routing/pathfind_test.go b/routing/pathfind_test.go index 280dd64a3a2a..433265b83ab3 100644 --- a/routing/pathfind_test.go +++ b/routing/pathfind_test.go @@ -2025,9 +2025,10 @@ func TestProbabilityRouting(t *testing.T) { t.Parallel() testCases := []struct { - name string - p10, p11, p20 float64 - expectedChan uint64 + name string + p10, p11, p20 float64 + minProbability float64 + expectedChan uint64 }{ // Test two variations with probabilities that should multiply // to the same total route probability. In both cases the three @@ -2039,12 +2040,14 @@ func TestProbabilityRouting(t *testing.T) { { name: "three hop 1", p10: 0.8, p11: 0.5, p20: 0.7, - expectedChan: 10, + minProbability: 0.1, + expectedChan: 10, }, { name: "three hop 2", p10: 0.5, p11: 0.8, p20: 0.7, - expectedChan: 10, + minProbability: 0.1, + expectedChan: 10, }, // If the probability of the two hop route is increased, its @@ -2055,20 +2058,42 @@ func TestProbabilityRouting(t *testing.T) { { name: "two hop higher cost", p10: 0.5, p11: 0.8, p20: 0.85, - expectedChan: 20, + minProbability: 0.1, + expectedChan: 20, + }, + + // If the same probabilities are used with a probability lower bound of + // 0.5, we expect the three hop route with probability 0.4 to be + // excluded and the two hop route to be picked. + { + name: "probability limit", + p10: 0.8, p11: 0.5, p20: 0.7, + minProbability: 0.5, + expectedChan: 20, + }, + + // With a probability limit above the probability of both routes, we + // expect no route to be returned. This expectation is signaled by using + // expected channel 0. + { + name: "probability limit no routes", + p10: 0.8, p11: 0.5, p20: 0.7, + minProbability: 0.8, + expectedChan: 0, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { testProbabilityRouting( - t, tc.p10, tc.p11, tc.p20, tc.expectedChan, + t, tc.p10, tc.p11, tc.p20, + tc.minProbability, tc.expectedChan, ) }) } } -func testProbabilityRouting(t *testing.T, p10, p11, p20 float64, +func testProbabilityRouting(t *testing.T, p10, p11, p20, minProbability float64, expectedChan uint64) { t.Parallel() @@ -2079,7 +2104,6 @@ func testProbabilityRouting(t *testing.T, p10, p11, p20 float64, testChannels := []*testChannel{ symmetricTestChannel("roasbeef", "a1", 100000, &testChannelPolicy{}), symmetricTestChannel("roasbeef", "b", 100000, &testChannelPolicy{}), - symmetricTestChannel("roasbeef", "c", 100000, &testChannelPolicy{}), symmetricTestChannel("a1", "a2", 100000, &testChannelPolicy{ Expiry: 144, FeeBaseMsat: lnwire.NewMSatFromSatoshis(5), @@ -2135,9 +2159,16 @@ func testProbabilityRouting(t *testing.T, p10, p11, p20 float64, FeeLimit: noFeeLimit, ProbabilitySource: probabilitySource, PaymentAttemptPenalty: lnwire.NewMSatFromSatoshis(10), + MinProbability: minProbability, }, sourceVertex, target, paymentAmt, ) + if expectedChan == 0 { + if err == nil || !IsError(err, ErrNoPathFound) { + t.Fatalf("expected no path found, but got %v", err) + } + return + } if err != nil { t.Fatal(err) } From ee3837aa45583247b2b6b1cfcc49a527ebda2bf6 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Tue, 19 Mar 2019 17:09:27 +0100 Subject: [PATCH 08/11] routing: global probability based mission control Previously every payment had its own local mission control state which was in effect only for that payment. In this commit most of the local state is removed and payments all tap into the global mission control probability estimator. Furthermore the decay time of pruned edges and nodes is extended, so that observations about the network can better benefit future payment processes. Last, the probability function is transformed from a binary output to a gradual curve, allowing for a better trade off between candidate routes. --- lnd_test.go | 11 +- lnrpc/routerrpc/router_backend.go | 3 +- lnrpc/routerrpc/router_backend_test.go | 6 +- lntest/node.go | 6 + routing/missioncontrol.go | 280 ++++++++++++++----------- routing/missioncontrol_test.go | 67 ++++++ routing/pathfind.go | 5 +- routing/pathfind_test.go | 9 +- routing/payment_session.go | 80 +++---- routing/payment_session_test.go | 3 +- routing/router.go | 58 ++--- 11 files changed, 318 insertions(+), 210 deletions(-) create mode 100644 routing/missioncontrol_test.go diff --git a/lnd_test.go b/lnd_test.go index bf48c1bca67e..a1dbb784ce5c 100644 --- a/lnd_test.go +++ b/lnd_test.go @@ -31,6 +31,7 @@ import ( "github.com/go-errors/errors" "github.com/lightningnetwork/lnd/chanbackup" "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/routerrpc" "github.com/lightningnetwork/lnd/lntest" "github.com/lightningnetwork/lnd/lnwire" "golang.org/x/net/context" @@ -8325,8 +8326,14 @@ out: // failed payment. shutdownAndAssert(net, t, carol) - // TODO(roasbeef): mission control - time.Sleep(time.Second * 5) + // Reset mission control to forget the temporary channel failure above. + ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) + _, err = net.Alice.RouterClient.ResetMissionControl( + ctxt, &routerrpc.ResetMissionControlRequest{}, + ) + if err != nil { + t.Fatalf("unable to reset mission control: %v", err) + } sendReq = &lnrpc.SendRequest{ PaymentRequest: carolInvoice.PaymentRequest, diff --git a/lnrpc/routerrpc/router_backend.go b/lnrpc/routerrpc/router_backend.go index dfda2109af6d..9e399e8fc7f2 100644 --- a/lnrpc/routerrpc/router_backend.go +++ b/lnrpc/routerrpc/router_backend.go @@ -125,7 +125,8 @@ func (r *RouterBackend) QueryRoutes(ctx context.Context, restrictions := &routing.RestrictParams{ FeeLimit: feeLimit, ProbabilitySource: func(node route.Vertex, - edge routing.EdgeLocator) float64 { + edge routing.EdgeLocator, + amt lnwire.MilliSatoshi) float64 { if _, ok := ignoredNodes[node]; ok { return 0 diff --git a/lnrpc/routerrpc/router_backend_test.go b/lnrpc/routerrpc/router_backend_test.go index f0913359c29d..b797ae9cf5db 100644 --- a/lnrpc/routerrpc/router_backend_test.go +++ b/lnrpc/routerrpc/router_backend_test.go @@ -81,19 +81,19 @@ func TestQueryRoutes(t *testing.T) { } if restrictions.ProbabilitySource(route.Vertex{}, - ignoredEdge, + ignoredEdge, 0, ) != 0 { t.Fatal("expecting 0% probability for ignored edge") } if restrictions.ProbabilitySource(ignoreNodeVertex, - routing.EdgeLocator{}, + routing.EdgeLocator{}, 0, ) != 0 { t.Fatal("expecting 0% probability for ignored node") } if restrictions.ProbabilitySource(route.Vertex{}, - routing.EdgeLocator{}, + routing.EdgeLocator{}, 0, ) != 1 { t.Fatal("expecting 100% probability") } diff --git a/lntest/node.go b/lntest/node.go index 3afbd5d0576d..683e71c536a4 100644 --- a/lntest/node.go +++ b/lntest/node.go @@ -28,6 +28,7 @@ import ( "github.com/lightningnetwork/lnd/chanbackup" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" + "github.com/lightningnetwork/lnd/lnrpc/routerrpc" "github.com/lightningnetwork/lnd/macaroons" ) @@ -243,6 +244,10 @@ type HarnessNode struct { lnrpc.WalletUnlockerClient invoicesrpc.InvoicesClient + + // RouterClient cannot be embedded, because a name collision would occur + // on the main rpc SendPayment. + RouterClient routerrpc.RouterClient } // Assert *HarnessNode implements the lnrpc.LightningClient interface. @@ -492,6 +497,7 @@ func (hn *HarnessNode) initLightningClient(conn *grpc.ClientConn) error { // HarnessNode directly for normal rpc operations. hn.LightningClient = lnrpc.NewLightningClient(conn) hn.InvoicesClient = invoicesrpc.NewInvoicesClient(conn) + hn.RouterClient = routerrpc.NewRouterClient(conn) // Set the harness node's pubkey to what the node claims in GetInfo. err := hn.FetchNodeInfo() diff --git a/routing/missioncontrol.go b/routing/missioncontrol.go index ac03ceff67ca..760703200fd9 100644 --- a/routing/missioncontrol.go +++ b/routing/missioncontrol.go @@ -1,6 +1,7 @@ package routing import ( + "math" "sync" "time" @@ -13,48 +14,23 @@ import ( ) const ( - // vertexDecay is the decay period of colored vertexes added to - // missionControl. Once vertexDecay passes after an entry has been - // added to the prune view, it is garbage collected. This value is - // larger than edgeDecay as an edge failure typical indicates an - // unbalanced channel, while a vertex failure indicates a node is not - // online and active. - vertexDecay = time.Duration(time.Minute * 5) - - // edgeDecay is the decay period of colored edges added to - // missionControl. Once edgeDecay passed after an entry has been added, - // it is garbage collected. This value is smaller than vertexDecay as - // an edge related failure during payment sending typically indicates - // that a channel was unbalanced, a condition which may quickly change. - // - // TODO(roasbeef): instead use random delay on each? - edgeDecay = time.Duration(time.Second * 5) + // defaultPenaltyHalfLife is the default half-life duration. The + // half-life duration defines after how much time a penalized node or + // channel is back at 50% probability. + defaultPenaltyHalfLife = time.Hour ) // MissionControl contains state which summarizes the past attempts of HTLC -// routing by external callers when sending payments throughout the network. -// missionControl remembers the outcome of these past routing attempts (success -// and failure), and is able to provide hints/guidance to future HTLC routing -// attempts. missionControl maintains a decaying network view of the -// edges/vertexes that should be marked as "pruned" during path finding. This -// graph view acts as a shared memory during HTLC payment routing attempts. -// With each execution, if an error is encountered, based on the type of error -// and the location of the error within the route, an edge or vertex is added -// to the view. Later sending attempts will then query the view for all the -// vertexes/edges that should be ignored. Items in the view decay after a set -// period of time, allowing the view to be dynamic w.r.t network changes. +// routing by external callers when sending payments throughout the network. It +// acts as a shared memory during routing attempts with the goal to optimize the +// payment attempt success rate. +// +// Failed payment attempts are reported to mission control. These reports are +// used to track the time of the last node or channel level failure. The time +// since the last failure is used to estimate a success probability that is fed +// into the path finding process for subsequent payment attempts. type MissionControl struct { - // failedEdges maps a short channel ID to be pruned, to the time that - // it was added to the prune view. Edges are added to this map if a - // caller reports to missionControl a failure localized to that edge - // when sending a payment. - failedEdges map[EdgeLocator]time.Time - - // failedVertexes maps a node's public key that should be pruned, to - // the time that it was added to the prune view. Vertexes are added to - // this map if a caller reports to missionControl a failure localized - // to that particular vertex. - failedVertexes map[route.Vertex]time.Time + history map[route.Vertex]*nodeHistory graph *channeldb.ChannelGraph @@ -62,6 +38,14 @@ type MissionControl struct { queryBandwidth func(*channeldb.ChannelEdgeInfo) lnwire.MilliSatoshi + // now is expected to return the current time. It is supplied as an + // external function to enable deterministic unit tests. + now func() time.Time + + // penaltyHalfLife defines after how much time a penalized node or + // channel is back at 50% probability. + penaltyHalfLife time.Duration + sync.Mutex // TODO(roasbeef): further counters, if vertex continually unavailable, @@ -70,83 +54,41 @@ type MissionControl struct { // TODO(roasbeef): also add favorable metrics for nodes } -// NewMissionControl returns a new instance of missionControl. -// -// TODO(roasbeef): persist memory -func NewMissionControl(g *channeldb.ChannelGraph, selfNode *channeldb.LightningNode, - qb func(*channeldb.ChannelEdgeInfo) lnwire.MilliSatoshi) *MissionControl { +// nodeHistory contains a summary of payment attempt outcomes involving a +// particular node. +type nodeHistory struct { + // lastFail is the last time a node level failure occurred, if any. + lastFail *time.Time - return &MissionControl{ - failedEdges: make(map[EdgeLocator]time.Time), - failedVertexes: make(map[route.Vertex]time.Time), - selfNode: selfNode, - queryBandwidth: qb, - graph: g, - } + // channelLastFail tracks history per channel, if available for that + // channel. + channelLastFail map[uint64]*channelHistory } -// graphPruneView is a filter of sorts that path finding routines should -// consult during the execution. Any edges or vertexes within the view should -// be ignored during path finding. The contents of the view reflect the current -// state of the wider network from the PoV of mission control compiled via HTLC -// routing attempts in the past. -type graphPruneView struct { - edges map[EdgeLocator]struct{} +// channelHistory contains a summary of payment attempt outcomes involving a +// particular channel. +type channelHistory struct { + // lastFail is the last time a channel level failure occurred. + lastFail time.Time - vertexes map[route.Vertex]struct{} + // minPenalizeAmt is the minimum amount for which to take this failure + // into account. + minPenalizeAmt lnwire.MilliSatoshi } -// GraphPruneView returns a new graphPruneView instance which is to be -// consulted during path finding. If a vertex/edge is found within the returned -// prune view, it is to be ignored as a goroutine has had issues routing -// through it successfully. Within this method the main view of the -// missionControl is garbage collected as entries are detected to be "stale". -func (m *MissionControl) graphPruneView() graphPruneView { - // First, we'll grab the current time, this value will be used to - // determine if an entry is stale or not. - now := time.Now() - - m.Lock() - - // For each of the vertexes that have been added to the prune view, if - // it is now "stale", then we'll ignore it and avoid adding it to the - // view we'll return. - vertexes := make(map[route.Vertex]struct{}) - for vertex, pruneTime := range m.failedVertexes { - if now.Sub(pruneTime) >= vertexDecay { - log.Tracef("Pruning decayed failure report for vertex %v "+ - "from Mission Control", vertex) - - delete(m.failedVertexes, vertex) - continue - } - - vertexes[vertex] = struct{}{} - } - - // We'll also do the same for edges, but use the edgeDecay this time - // rather than the decay for vertexes. - edges := make(map[EdgeLocator]struct{}) - for edge, pruneTime := range m.failedEdges { - if now.Sub(pruneTime) >= edgeDecay { - log.Tracef("Pruning decayed failure report for edge %v "+ - "from Mission Control", edge) - - delete(m.failedEdges, edge) - continue - } - - edges[edge] = struct{}{} - } - - m.Unlock() - - log.Debugf("Mission Control returning prune view of %v edges, %v "+ - "vertexes", len(edges), len(vertexes)) +// NewMissionControl returns a new instance of missionControl. +// +// TODO(roasbeef): persist memory +func NewMissionControl(g *channeldb.ChannelGraph, selfNode *channeldb.LightningNode, + qb func(*channeldb.ChannelEdgeInfo) lnwire.MilliSatoshi) *MissionControl { - return graphPruneView{ - edges: edges, - vertexes: vertexes, + return &MissionControl{ + history: make(map[route.Vertex]*nodeHistory), + selfNode: selfNode, + queryBandwidth: qb, + graph: g, + now: time.Now, + penaltyHalfLife: defaultPenaltyHalfLife, } } @@ -157,8 +99,6 @@ func (m *MissionControl) graphPruneView() graphPruneView { func (m *MissionControl) newPaymentSession(routeHints [][]zpay32.HopHint, target route.Vertex) (*paymentSession, error) { - viewSnapshot := m.graphPruneView() - edges := make(map[route.Vertex][]*channeldb.ChannelEdgePolicy) // Traverse through all of the available hop hints and include them in @@ -222,10 +162,9 @@ func (m *MissionControl) newPaymentSession(routeHints [][]zpay32.HopHint, } return &paymentSession{ - pruneViewSnapshot: viewSnapshot, additionalEdges: edges, bandwidthHints: bandwidthHints, - errFailedPolicyChans: make(map[EdgeLocator]struct{}), + errFailedPolicyChans: make(map[nodeChannel]struct{}), mc: m, pathFinder: findPath, }, nil @@ -235,8 +174,7 @@ func (m *MissionControl) newPaymentSession(routeHints [][]zpay32.HopHint, // used for failure reporting to missioncontrol. func (m *MissionControl) newPaymentSessionForRoute(preBuiltRoute *route.Route) *paymentSession { return &paymentSession{ - pruneViewSnapshot: m.graphPruneView(), - errFailedPolicyChans: make(map[EdgeLocator]struct{}), + errFailedPolicyChans: make(map[nodeChannel]struct{}), mc: m, preBuiltRoute: preBuiltRoute, } @@ -281,7 +219,115 @@ func generateBandwidthHints(sourceNode *channeldb.LightningNode, // if no payment attempts have been made. func (m *MissionControl) ResetHistory() { m.Lock() - m.failedEdges = make(map[EdgeLocator]time.Time) - m.failedVertexes = make(map[route.Vertex]time.Time) - m.Unlock() + defer m.Unlock() + + m.history = make(map[route.Vertex]*nodeHistory) + + log.Debugf("Mission control history cleared") +} + +// getEdgeProbability is expected to return the success probability of a payment +// from fromNode along edge. +func (m *MissionControl) getEdgeProbability(fromNode route.Vertex, + edge EdgeLocator, amt lnwire.MilliSatoshi) float64 { + + m.Lock() + defer m.Unlock() + + // Get the history for this node. If there is no history available, + // assume that it's success probability is 1. After the attempt new + // information becomes available to adjust this probability. + nodeHistory, ok := m.history[fromNode] + if !ok { + return 1 + } + + return m.getEdgeProbabilityForNode(nodeHistory, edge.ChannelID, amt) +} + +// getEdgeProbabilityForNode estimates the probability of successfully +// traversing a channel based on the node history. +func (m *MissionControl) getEdgeProbabilityForNode(nodeHistory *nodeHistory, + channelID uint64, amt lnwire.MilliSatoshi) float64 { + + // Calculate the last failure of the given edge. A node failure is + // considered a failure that would have affected every edge. Therefore + // we insert a node level failure into the history of every channel. + lastFailure := nodeHistory.lastFail + + // Take into account a minimum penalize amount. For balance errors, a + // failure may be reported with such a minimum to prevent too aggresive + // penalization. + channelHistory, ok := nodeHistory.channelLastFail[channelID] + if ok && channelHistory.minPenalizeAmt <= amt { + if lastFailure == nil || + channelHistory.lastFail.After(*lastFailure) { + + lastFailure = &channelHistory.lastFail + } + } + + if lastFailure == nil { + return 1 + } + + timeSinceLastFailure := m.now().Sub(*lastFailure) + + // Calculate success probability. It is an exponential curve that brings + // the probability down to zero when a failure occurs. From there it + // recovers asymptotically back to 1. The rate at which this happens is + // controlled by the penaltyHalfLife parameter. + exp := -timeSinceLastFailure.Hours() / m.penaltyHalfLife.Hours() + probability := 1 - math.Pow(2, exp) + + return probability +} + +// createHistoryIfNotExists returns the history for the given node. If the node +// is yet unknown, it will create an empty history structure. +func (m *MissionControl) createHistoryIfNotExists(vertex route.Vertex) *nodeHistory { + if node, ok := m.history[vertex]; ok { + return node + } + + node := &nodeHistory{ + channelLastFail: make(map[uint64]*channelHistory), + } + m.history[vertex] = node + + return node +} + +// reportVertexFailure reports a node level failure. +func (m *MissionControl) reportVertexFailure(v route.Vertex) { + log.Debugf("Reporting vertex %v failure to Mission Control", v) + + now := m.now() + + m.Lock() + defer m.Unlock() + + history := m.createHistoryIfNotExists(v) + history.lastFail = &now +} + +// reportEdgeFailure reports a channel level failure. +// +// TODO(roasbeef): also add value attempted to send and capacity of channel +func (m *MissionControl) reportEdgeFailure(failedEdge edge, + minPenalizeAmt lnwire.MilliSatoshi) { + + log.Debugf("Reporting channel %v failure to Mission Control", + failedEdge.channel) + + now := m.now() + + m.Lock() + defer m.Unlock() + + history := m.createHistoryIfNotExists(failedEdge.from) + history.channelLastFail[failedEdge.channel] = &channelHistory{ + lastFail: now, + minPenalizeAmt: minPenalizeAmt, + } } diff --git a/routing/missioncontrol_test.go b/routing/missioncontrol_test.go new file mode 100644 index 000000000000..39ebb5bdd200 --- /dev/null +++ b/routing/missioncontrol_test.go @@ -0,0 +1,67 @@ +package routing + +import ( + "testing" + "time" + + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing/route" +) + +// TestMissionControl tests mission control probability estimation. +func TestMissionControl(t *testing.T) { + now := testTime + + mc := NewMissionControl(nil, nil, nil) + mc.now = func() time.Time { return now } + mc.penaltyHalfLife = 30 * time.Minute + + testTime := time.Date(2018, time.January, 9, 14, 00, 00, 0, time.UTC) + + testNode := route.Vertex{} + testEdge := edge{ + channel: 123, + } + + expectP := func(amt lnwire.MilliSatoshi, expected float64) { + t.Helper() + + p := mc.getEdgeProbability( + testNode, EdgeLocator{ChannelID: testEdge.channel}, + amt, + ) + if p != expected { + t.Fatalf("unexpected probability %v", p) + } + } + + // Initial probability is expected to be 1. + expectP(1000, 1) + + // Expect probability to be zero after reporting the edge as failed. + mc.reportEdgeFailure(testEdge, 1000) + expectP(1000, 0) + + // As we reported with a min penalization amt, a lower amt than reported + // should be unaffected. + expectP(500, 1) + + // Edge decay started. + now = testTime.Add(30 * time.Minute) + expectP(1000, 0.5) + + // Edge fails again, this time without a min penalization amt. The edge + // should be penalized regardless of amount. + mc.reportEdgeFailure(testEdge, 0) + expectP(1000, 0) + expectP(500, 0) + + // Edge decay started. + now = testTime.Add(60 * time.Minute) + expectP(1000, 0.5) + + // A node level failure should bring probability of every channel back + // to zero. + mc.reportVertexFailure(testNode) + expectP(1000, 0) +} diff --git a/routing/pathfind.go b/routing/pathfind.go index fbd60682c64e..30ca41f36d70 100644 --- a/routing/pathfind.go +++ b/routing/pathfind.go @@ -242,7 +242,8 @@ type graphParams struct { 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, EdgeLocator) float64 + ProbabilitySource func(route.Vertex, EdgeLocator, + lnwire.MilliSatoshi) float64 // FeeLimit is a maximum fee amount allowed to be used on the path from // the source to the target. @@ -398,7 +399,7 @@ func findPath(g *graphParams, r *RestrictParams, source, target route.Vertex, // Request the success probability for this edge. locator := newEdgeLocator(edge) edgeProbability := r.ProbabilitySource( - fromVertex, *locator, + fromVertex, *locator, amountToSend, ) // If the probability is zero, there is no point in trying. diff --git a/routing/pathfind_test.go b/routing/pathfind_test.go index 433265b83ab3..8a5cf0687721 100644 --- a/routing/pathfind_test.go +++ b/routing/pathfind_test.go @@ -75,7 +75,7 @@ var ( // noProbabilitySource is used in testing to return the same probability 1 for // all edges. -func noProbabilitySource(route.Vertex, EdgeLocator) float64 { +func noProbabilitySource(route.Vertex, EdgeLocator, lnwire.MilliSatoshi) float64 { return 1 } @@ -2137,7 +2137,12 @@ func testProbabilityRouting(t *testing.T, p10, p11, p20, minProbability float64, target := testGraphInstance.aliasMap["target"] // Configure a probability source with the test parameters. - probabilitySource := func(node route.Vertex, edge EdgeLocator) float64 { + probabilitySource := func(node route.Vertex, edge EdgeLocator, + amt lnwire.MilliSatoshi) float64 { + + if amt == 0 { + t.Fatal("expected non-zero amount") + } switch edge.ChannelID { case 10: diff --git a/routing/payment_session.go b/routing/payment_session.go index bd4210fdc680..fca1fb09d120 100644 --- a/routing/payment_session.go +++ b/routing/payment_session.go @@ -2,7 +2,6 @@ package routing import ( "fmt" - "time" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/lnwire" @@ -18,8 +17,6 @@ import ( // loop if payment attempts take long enough. An additional set of edges can // also be provided to assist in reaching the payment's destination. type paymentSession struct { - pruneViewSnapshot graphPruneView - additionalEdges map[route.Vertex][]*channeldb.ChannelEdgePolicy bandwidthHints map[uint64]lnwire.MilliSatoshi @@ -28,7 +25,7 @@ type paymentSession struct { // source of policy related routing failures during this payment attempt. // We'll use this map to prune out channels when the first error may not // require pruning, but any subsequent ones do. - errFailedPolicyChans map[EdgeLocator]struct{} + errFailedPolicyChans map[nodeChannel]struct{} mc *MissionControl @@ -44,17 +41,7 @@ type paymentSession struct { // vertexDecay. However, the vertex will remain pruned for the *local* session. // This ensures we don't retry this vertex during the payment attempt. func (p *paymentSession) ReportVertexFailure(v route.Vertex) { - log.Debugf("Reporting vertex %v failure to Mission Control", v) - - // First, we'll add the failed vertex to our local prune view snapshot. - p.pruneViewSnapshot.vertexes[v] = struct{}{} - - // With the vertex added, we'll now report back to the global prune - // view, with this new piece of information so it can be utilized for - // new payment sessions. - p.mc.Lock() - p.mc.failedVertexes[v] = time.Now() - p.mc.Unlock() + p.mc.reportVertexFailure(v) } // ReportChannelFailure adds a channel to the graph prune view. The time the @@ -64,18 +51,10 @@ func (p *paymentSession) ReportVertexFailure(v route.Vertex) { // retrying an edge after its pruning has expired. // // TODO(roasbeef): also add value attempted to send and capacity of channel -func (p *paymentSession) ReportEdgeFailure(e *EdgeLocator) { - log.Debugf("Reporting edge %v failure to Mission Control", e) - - // First, we'll add the failed edge to our local prune view snapshot. - p.pruneViewSnapshot.edges[*e] = struct{}{} - - // With the edge added, we'll now report back to the global prune view, - // with this new piece of information so it can be utilized for new - // payment sessions. - p.mc.Lock() - p.mc.failedEdges[*e] = time.Now() - p.mc.Unlock() +func (p *paymentSession) ReportEdgeFailure(failedEdge edge, + minPenalizeAmt lnwire.MilliSatoshi) { + + p.mc.reportEdgeFailure(failedEdge, minPenalizeAmt) } // ReportChannelPolicyFailure handles a failure message that relates to a @@ -84,37 +63,28 @@ func (p *paymentSession) ReportEdgeFailure(e *EdgeLocator) { // edge as 'policy failed once'. The next time it fails, the whole node will be // pruned. This is to prevent nodes from keeping us busy by continuously sending // new channel updates. -func (p *paymentSession) ReportEdgePolicyFailure( - errSource route.Vertex, failedEdge *EdgeLocator) { +// +// TODO(joostjager): Move this logic into global mission control. +func (p *paymentSession) ReportEdgePolicyFailure(failedEdge edge) { + key := nodeChannel{ + node: failedEdge.from, + channel: failedEdge.channel, + } // Check to see if we've already reported a policy related failure for // this channel. If so, then we'll prune out the vertex. - _, ok := p.errFailedPolicyChans[*failedEdge] + _, ok := p.errFailedPolicyChans[key] if ok { // TODO(joostjager): is this aggressive pruning still necessary? // Just pruning edges may also work unless there is a huge // number of failing channels from that node? - p.ReportVertexFailure(errSource) + p.ReportVertexFailure(key.node) return } // Finally, we'll record a policy failure from this node and move on. - p.errFailedPolicyChans[*failedEdge] = struct{}{} -} - -func (p *paymentSession) getEdgeProbability(node route.Vertex, - edge EdgeLocator) float64 { - - if _, ok := p.pruneViewSnapshot.vertexes[node]; ok { - return 0 - } - - if _, ok := p.pruneViewSnapshot.edges[edge]; ok { - return 0 - } - - return 1 + p.errFailedPolicyChans[key] = struct{}{} } // RequestRoute returns a route which is likely to be capable for successfully @@ -142,15 +112,6 @@ func (p *paymentSession) RequestRoute(payment *LightningPayment, return nil, fmt.Errorf("pre-built route already tried") } - // Otherwise we actually need to perform path finding, so we'll obtain - // our current prune view snapshot. This view will only ever grow - // during the duration of this payment session, never shrinking. - pruneView := p.pruneViewSnapshot - - log.Debugf("Mission Control session using prune view of %v "+ - "edges, %v vertexes", len(pruneView.edges), - len(pruneView.vertexes)) - // If a route cltv limit was specified, we need to subtract the final // delta before passing it into path finding. The optimal path is // independent of the final cltv delta and the path finding algorithm is @@ -173,11 +134,12 @@ func (p *paymentSession) RequestRoute(payment *LightningPayment, bandwidthHints: p.bandwidthHints, }, &RestrictParams{ - ProbabilitySource: p.getEdgeProbability, + ProbabilitySource: p.mc.getEdgeProbability, FeeLimit: payment.FeeLimit, OutgoingChannelID: payment.OutgoingChannelID, CltvLimit: cltvLimit, PaymentAttemptPenalty: DefaultPaymentAttemptPenalty, + MinProbability: DefaultMinProbability, }, p.mc.selfNode.PubKeyBytes, payment.Target, payment.Amount, @@ -200,3 +162,9 @@ func (p *paymentSession) RequestRoute(payment *LightningPayment, return route, err } + +// nodeChannel is a combination of the node pubkey and one of its channels. +type nodeChannel struct { + node route.Vertex + channel uint64 +} diff --git a/routing/payment_session_test.go b/routing/payment_session_test.go index f730bc576711..3ac01f2a98d8 100644 --- a/routing/payment_session_test.go +++ b/routing/payment_session_test.go @@ -36,8 +36,7 @@ func TestRequestRoute(t *testing.T) { mc: &MissionControl{ selfNode: &channeldb.LightningNode{}, }, - pruneViewSnapshot: graphPruneView{}, - pathFinder: findPath, + pathFinder: findPath, } cltvLimit := uint32(30) diff --git a/routing/router.go b/routing/router.go index 5588dfe897f1..a186bc2f3210 100644 --- a/routing/router.go +++ b/routing/router.go @@ -298,6 +298,13 @@ func (e *EdgeLocator) String() string { return fmt.Sprintf("%v:%v", e.ChannelID, e.Direction) } +// edge is a combination of a channel and the node pubkeys of both of its +// endpoints. +type edge struct { + from, to route.Vertex + channel uint64 +} + // ChannelRouter is the layer 3 router within the Lightning stack. Below the // ChannelRouter is the HtlcSwitch, and below that is the Bitcoin blockchain // itself. The primary role of the ChannelRouter is to respond to queries for @@ -1802,7 +1809,9 @@ func (r *ChannelRouter) processSendError(paySession *paymentSession, // Always determine chan id ourselves, because a channel // update with id may not be available. - failedEdge, err := getFailedEdge(rt, route.Vertex(errVertex)) + failedEdge, failedAmt, err := getFailedEdge( + rt, route.Vertex(errVertex), + ) if err != nil { return true } @@ -1834,13 +1843,11 @@ func (r *ChannelRouter) processSendError(paySession *paymentSession, // update to fail? if !updateOk { paySession.ReportEdgeFailure( - failedEdge, + failedEdge, 0, ) } - paySession.ReportEdgePolicyFailure( - route.NewVertex(errSource), failedEdge, - ) + paySession.ReportEdgePolicyFailure(failedEdge) } switch onionErr := fErr.FailureMessage.(type) { @@ -1931,7 +1938,7 @@ func (r *ChannelRouter) processSendError(paySession *paymentSession, // the update and continue. case *lnwire.FailChannelDisabled: r.applyChannelUpdate(&onionErr.Update, errSource) - paySession.ReportEdgeFailure(failedEdge) + paySession.ReportEdgeFailure(failedEdge, 0) return false // It's likely that the outgoing channel didn't have @@ -1939,7 +1946,7 @@ func (r *ChannelRouter) processSendError(paySession *paymentSession, // now, and continue onwards with our path finding. case *lnwire.FailTemporaryChannelFailure: r.applyChannelUpdate(onionErr.Update, errSource) - paySession.ReportEdgeFailure(failedEdge) + paySession.ReportEdgeFailure(failedEdge, failedAmt) return false // If the send fail due to a node not having the @@ -1964,7 +1971,7 @@ func (r *ChannelRouter) processSendError(paySession *paymentSession, // returning errors in order to attempt to black list // another node. case *lnwire.FailUnknownNextPeer: - paySession.ReportEdgeFailure(failedEdge) + paySession.ReportEdgeFailure(failedEdge, 0) return false // If the node wasn't able to forward for which ever @@ -1995,14 +2002,12 @@ func (r *ChannelRouter) processSendError(paySession *paymentSession, // we'll prune the channel in both directions and // continue with the rest of the routes. case *lnwire.FailPermanentChannelFailure: - paySession.ReportEdgeFailure(&EdgeLocator{ - ChannelID: failedEdge.ChannelID, - Direction: 0, - }) - paySession.ReportEdgeFailure(&EdgeLocator{ - ChannelID: failedEdge.ChannelID, - Direction: 1, - }) + paySession.ReportEdgeFailure(failedEdge, 0) + paySession.ReportEdgeFailure(edge{ + from: failedEdge.to, + to: failedEdge.from, + channel: failedEdge.channel, + }, 0) return false default: @@ -2012,12 +2017,14 @@ func (r *ChannelRouter) processSendError(paySession *paymentSession, // getFailedEdge tries to locate the failing channel given a route and the // pubkey of the node that sent the error. It will assume that the error is -// associated with the outgoing channel of the error node. -func getFailedEdge(route *route.Route, errSource route.Vertex) ( - *EdgeLocator, error) { +// associated with the outgoing channel of the error node. As a second result, +// it returns the amount sent over the edge. +func getFailedEdge(route *route.Route, errSource route.Vertex) (edge, + lnwire.MilliSatoshi, error) { hopCount := len(route.Hops) fromNode := route.SourcePubKey + amt := route.TotalAmount for i, hop := range route.Hops { toNode := hop.PubKeyBytes @@ -2036,17 +2043,18 @@ func getFailedEdge(route *route.Route, errSource route.Vertex) ( // If the errSource is the final hop, we assume that the failing // channel is the incoming channel. if errSource == fromNode || finalHopFailing { - return newEdgeLocatorByPubkeys( - hop.ChannelID, - &fromNode, - &toNode, - ), nil + return edge{ + from: fromNode, + to: toNode, + channel: hop.ChannelID, + }, amt, nil } fromNode = toNode + amt = hop.AmtToForward } - return nil, fmt.Errorf("cannot find error source node in route") + return edge{}, 0, fmt.Errorf("cannot find error source node in route") } // applyChannelUpdate validates a channel update and if valid, applies it to the From d48b73b062203e9ee289b0a0a55728f45bdbc15d Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Fri, 10 May 2019 10:38:31 +0200 Subject: [PATCH 09/11] routing+routerrpc: expose mission control state over rpc This commit exposes mission control state for rpc for development purposes. --- lnrpc/routerrpc/router.pb.go | 418 +++++++++++++++++++++++++------ lnrpc/routerrpc/router.proto | 43 ++++ lnrpc/routerrpc/router_server.go | 49 ++++ routing/missioncontrol.go | 94 +++++++ routing/missioncontrol_test.go | 10 + 5 files changed, 536 insertions(+), 78 deletions(-) diff --git a/lnrpc/routerrpc/router.pb.go b/lnrpc/routerrpc/router.pb.go index 3b6c23ecb539..4f14d77e4bc9 100644 --- a/lnrpc/routerrpc/router.pb.go +++ b/lnrpc/routerrpc/router.pb.go @@ -757,6 +757,213 @@ func (m *ResetMissionControlResponse) XXX_DiscardUnknown() { var xxx_messageInfo_ResetMissionControlResponse proto.InternalMessageInfo +type QueryMissionControlRequest struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *QueryMissionControlRequest) Reset() { *m = QueryMissionControlRequest{} } +func (m *QueryMissionControlRequest) String() string { return proto.CompactTextString(m) } +func (*QueryMissionControlRequest) ProtoMessage() {} +func (*QueryMissionControlRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_7a0613f69d37b0a5, []int{10} +} + +func (m *QueryMissionControlRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_QueryMissionControlRequest.Unmarshal(m, b) +} +func (m *QueryMissionControlRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_QueryMissionControlRequest.Marshal(b, m, deterministic) +} +func (m *QueryMissionControlRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_QueryMissionControlRequest.Merge(m, src) +} +func (m *QueryMissionControlRequest) XXX_Size() int { + return xxx_messageInfo_QueryMissionControlRequest.Size(m) +} +func (m *QueryMissionControlRequest) XXX_DiscardUnknown() { + xxx_messageInfo_QueryMissionControlRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_QueryMissionControlRequest proto.InternalMessageInfo + +/// QueryMissionControlResponse contains mission control state per node. +type QueryMissionControlResponse struct { + Nodes []*NodeHistory `protobuf:"bytes,1,rep,name=nodes,proto3" json:"nodes,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *QueryMissionControlResponse) Reset() { *m = QueryMissionControlResponse{} } +func (m *QueryMissionControlResponse) String() string { return proto.CompactTextString(m) } +func (*QueryMissionControlResponse) ProtoMessage() {} +func (*QueryMissionControlResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_7a0613f69d37b0a5, []int{11} +} + +func (m *QueryMissionControlResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_QueryMissionControlResponse.Unmarshal(m, b) +} +func (m *QueryMissionControlResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_QueryMissionControlResponse.Marshal(b, m, deterministic) +} +func (m *QueryMissionControlResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_QueryMissionControlResponse.Merge(m, src) +} +func (m *QueryMissionControlResponse) XXX_Size() int { + return xxx_messageInfo_QueryMissionControlResponse.Size(m) +} +func (m *QueryMissionControlResponse) XXX_DiscardUnknown() { + xxx_messageInfo_QueryMissionControlResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_QueryMissionControlResponse proto.InternalMessageInfo + +func (m *QueryMissionControlResponse) GetNodes() []*NodeHistory { + if m != nil { + return m.Nodes + } + return nil +} + +/// NodeHistory contains the mission control state for a particular node. +type NodeHistory struct { + /// Node pubkey + Pubkey []byte `protobuf:"bytes,1,opt,name=pubkey,proto3" json:"pubkey,omitempty"` + /// Time stamp of last failure. Set to zero if no failure happened yet. + LastFailTime int64 `protobuf:"varint,2,opt,name=last_fail_time,json=lastFailTime,proto3" json:"last_fail_time,omitempty"` + /// Estimation of success probability for channels not in the channel array. + OtherChanSuccessProb float32 `protobuf:"fixed32,3,opt,name=other_chan_success_prob,json=otherChanSuccessProb,proto3" json:"other_chan_success_prob,omitempty"` + /// Historical information of particular channels. + Channels []*ChannelHistory `protobuf:"bytes,4,rep,name=channels,proto3" json:"channels,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *NodeHistory) Reset() { *m = NodeHistory{} } +func (m *NodeHistory) String() string { return proto.CompactTextString(m) } +func (*NodeHistory) ProtoMessage() {} +func (*NodeHistory) Descriptor() ([]byte, []int) { + return fileDescriptor_7a0613f69d37b0a5, []int{12} +} + +func (m *NodeHistory) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_NodeHistory.Unmarshal(m, b) +} +func (m *NodeHistory) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_NodeHistory.Marshal(b, m, deterministic) +} +func (m *NodeHistory) XXX_Merge(src proto.Message) { + xxx_messageInfo_NodeHistory.Merge(m, src) +} +func (m *NodeHistory) XXX_Size() int { + return xxx_messageInfo_NodeHistory.Size(m) +} +func (m *NodeHistory) XXX_DiscardUnknown() { + xxx_messageInfo_NodeHistory.DiscardUnknown(m) +} + +var xxx_messageInfo_NodeHistory proto.InternalMessageInfo + +func (m *NodeHistory) GetPubkey() []byte { + if m != nil { + return m.Pubkey + } + return nil +} + +func (m *NodeHistory) GetLastFailTime() int64 { + if m != nil { + return m.LastFailTime + } + return 0 +} + +func (m *NodeHistory) GetOtherChanSuccessProb() float32 { + if m != nil { + return m.OtherChanSuccessProb + } + return 0 +} + +func (m *NodeHistory) GetChannels() []*ChannelHistory { + if m != nil { + return m.Channels + } + return nil +} + +/// NodeHistory contains the mission control state for a particular channel. +type ChannelHistory struct { + /// Short channel id + ChannelId uint64 `protobuf:"varint,1,opt,name=channel_id,json=channelId,proto3" json:"channel_id,omitempty"` + /// Time stamp of last failure. + LastFailTime int64 `protobuf:"varint,2,opt,name=last_fail_time,json=lastFailTime,proto3" json:"last_fail_time,omitempty"` + /// Minimum penalization amount. + MinPenalizeAmt int64 `protobuf:"varint,3,opt,name=min_penalize_amt,json=minPenalizeAmt,proto3" json:"min_penalize_amt,omitempty"` + /// Estimation of success probability for this channel. + SuccessProb float32 `protobuf:"fixed32,4,opt,name=success_prob,json=successProb,proto3" json:"success_prob,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ChannelHistory) Reset() { *m = ChannelHistory{} } +func (m *ChannelHistory) String() string { return proto.CompactTextString(m) } +func (*ChannelHistory) ProtoMessage() {} +func (*ChannelHistory) Descriptor() ([]byte, []int) { + return fileDescriptor_7a0613f69d37b0a5, []int{13} +} + +func (m *ChannelHistory) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ChannelHistory.Unmarshal(m, b) +} +func (m *ChannelHistory) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ChannelHistory.Marshal(b, m, deterministic) +} +func (m *ChannelHistory) XXX_Merge(src proto.Message) { + xxx_messageInfo_ChannelHistory.Merge(m, src) +} +func (m *ChannelHistory) XXX_Size() int { + return xxx_messageInfo_ChannelHistory.Size(m) +} +func (m *ChannelHistory) XXX_DiscardUnknown() { + xxx_messageInfo_ChannelHistory.DiscardUnknown(m) +} + +var xxx_messageInfo_ChannelHistory proto.InternalMessageInfo + +func (m *ChannelHistory) GetChannelId() uint64 { + if m != nil { + return m.ChannelId + } + return 0 +} + +func (m *ChannelHistory) GetLastFailTime() int64 { + if m != nil { + return m.LastFailTime + } + return 0 +} + +func (m *ChannelHistory) GetMinPenalizeAmt() int64 { + if m != nil { + return m.MinPenalizeAmt + } + return 0 +} + +func (m *ChannelHistory) GetSuccessProb() float32 { + if m != nil { + return m.SuccessProb + } + return 0 +} + func init() { proto.RegisterEnum("routerrpc.Failure_FailureCode", Failure_FailureCode_name, Failure_FailureCode_value) proto.RegisterType((*PaymentRequest)(nil), "routerrpc.PaymentRequest") @@ -769,89 +976,105 @@ func init() { proto.RegisterType((*ChannelUpdate)(nil), "routerrpc.ChannelUpdate") proto.RegisterType((*ResetMissionControlRequest)(nil), "routerrpc.ResetMissionControlRequest") proto.RegisterType((*ResetMissionControlResponse)(nil), "routerrpc.ResetMissionControlResponse") + proto.RegisterType((*QueryMissionControlRequest)(nil), "routerrpc.QueryMissionControlRequest") + proto.RegisterType((*QueryMissionControlResponse)(nil), "routerrpc.QueryMissionControlResponse") + proto.RegisterType((*NodeHistory)(nil), "routerrpc.NodeHistory") + proto.RegisterType((*ChannelHistory)(nil), "routerrpc.ChannelHistory") } func init() { proto.RegisterFile("routerrpc/router.proto", fileDescriptor_7a0613f69d37b0a5) } var fileDescriptor_7a0613f69d37b0a5 = []byte{ - // 1220 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x56, 0xdd, 0x72, 0xdb, 0xb6, - 0x12, 0x3e, 0xf2, 0x8f, 0x7e, 0x56, 0x92, 0x4d, 0xc3, 0x76, 0x22, 0xcb, 0x71, 0xe2, 0xf0, 0x9c, - 0x93, 0x7a, 0x3a, 0x1d, 0x7b, 0xaa, 0x4e, 0x72, 0xd9, 0x0e, 0x23, 0x41, 0x35, 0x27, 0x12, 0xa9, - 0x82, 0x92, 0x13, 0xb7, 0x17, 0x18, 0x58, 0x84, 0x25, 0xd6, 0x12, 0xc9, 0x90, 0x50, 0x5b, 0xbf, - 0x40, 0x1f, 0xa8, 0xef, 0xd0, 0xeb, 0x5e, 0xf7, 0x6d, 0x3a, 0x00, 0x48, 0x59, 0x76, 0x9c, 0xe9, - 0x95, 0x88, 0xef, 0x5b, 0xec, 0x62, 0x17, 0xbb, 0x1f, 0x04, 0x4f, 0x92, 0x68, 0x21, 0x78, 0x92, - 0xc4, 0xe3, 0x33, 0xfd, 0x75, 0x1a, 0x27, 0x91, 0x88, 0x50, 0x65, 0x89, 0x37, 0x2b, 0x49, 0x3c, - 0xd6, 0xa8, 0xf9, 0x67, 0x01, 0xb6, 0x06, 0xec, 0x76, 0xce, 0x43, 0x41, 0xf8, 0xc7, 0x05, 0x4f, - 0x05, 0x7a, 0x0a, 0xa5, 0x98, 0xdd, 0xd2, 0x84, 0x7f, 0x6c, 0x14, 0x8e, 0x0b, 0x27, 0x15, 0x52, - 0x8c, 0xd9, 0x2d, 0xe1, 0x1f, 0x91, 0x09, 0xf5, 0x6b, 0xce, 0xe9, 0x2c, 0x98, 0x07, 0x82, 0xa6, - 0x4c, 0x34, 0xd6, 0x8e, 0x0b, 0x27, 0xeb, 0xa4, 0x7a, 0xcd, 0x79, 0x4f, 0x62, 0x1e, 0x13, 0xe8, - 0x08, 0x60, 0x3c, 0x13, 0xbf, 0x68, 0xa3, 0xc6, 0xfa, 0x71, 0xe1, 0x64, 0x93, 0x54, 0x24, 0xa2, - 0x2c, 0xd0, 0x17, 0xb0, 0x2d, 0x82, 0x39, 0x8f, 0x16, 0x82, 0xa6, 0x7c, 0x1c, 0x85, 0x7e, 0xda, - 0xd8, 0x50, 0x36, 0x5b, 0x19, 0xec, 0x69, 0x14, 0x9d, 0xc2, 0x6e, 0xb4, 0x10, 0x93, 0x28, 0x08, - 0x27, 0x74, 0x3c, 0x65, 0x61, 0xc8, 0x67, 0x34, 0xf0, 0x1b, 0x9b, 0x2a, 0xe2, 0x4e, 0x4e, 0xb5, - 0x35, 0x63, 0xfb, 0xe6, 0xcf, 0xb0, 0xbd, 0x4c, 0x23, 0x8d, 0xa3, 0x30, 0xe5, 0xe8, 0x00, 0xca, - 0x32, 0x8f, 0x29, 0x4b, 0xa7, 0x2a, 0x91, 0x1a, 0x91, 0x79, 0x9d, 0xb3, 0x74, 0x8a, 0x0e, 0xa1, - 0x12, 0x27, 0x9c, 0x06, 0x73, 0x36, 0xe1, 0x2a, 0x8b, 0x1a, 0x29, 0xc7, 0x09, 0xb7, 0xe5, 0x1a, - 0xbd, 0x80, 0x6a, 0xac, 0x5d, 0x51, 0x9e, 0x24, 0x2a, 0x87, 0x0a, 0x81, 0x0c, 0xc2, 0x49, 0x62, - 0x7e, 0x0b, 0xdb, 0x44, 0xd6, 0xb2, 0xcb, 0x79, 0x5e, 0x33, 0x04, 0x1b, 0x3e, 0x4f, 0x45, 0x16, - 0x47, 0x7d, 0xcb, 0x3a, 0xb2, 0xf9, 0x6a, 0xa1, 0x8a, 0x6c, 0x2e, 0x6b, 0x64, 0xfa, 0x60, 0xdc, - 0xed, 0xcf, 0x0e, 0x7b, 0x02, 0x86, 0xbc, 0x1f, 0x99, 0xae, 0xac, 0xf1, 0x5c, 0xee, 0x2a, 0xa8, - 0x5d, 0x5b, 0x19, 0xde, 0xe5, 0xbc, 0x9f, 0x32, 0x81, 0x5e, 0xe9, 0x12, 0xd2, 0x59, 0x34, 0xbe, - 0xa1, 0x3e, 0x9f, 0xb1, 0xdb, 0xcc, 0x7d, 0x5d, 0xc2, 0xbd, 0x68, 0x7c, 0xd3, 0x91, 0xa0, 0xf9, - 0x13, 0x20, 0x8f, 0x87, 0xfe, 0x30, 0x52, 0xb1, 0xf2, 0x83, 0xbe, 0x84, 0x5a, 0x9e, 0xdc, 0x4a, - 0x61, 0xf2, 0x84, 0x55, 0x71, 0x4c, 0xd8, 0x54, 0xad, 0xa2, 0xdc, 0x56, 0x5b, 0xb5, 0xd3, 0x59, - 0x28, 0xfb, 0x45, 0xbb, 0xd1, 0x94, 0x49, 0x61, 0xf7, 0x9e, 0xf3, 0x2c, 0x8b, 0x26, 0xc8, 0x32, - 0xea, 0xb2, 0x16, 0x96, 0x65, 0x55, 0x6b, 0xf4, 0x15, 0x94, 0xae, 0x59, 0x30, 0x5b, 0x24, 0xb9, - 0x63, 0x74, 0xba, 0xec, 0xc8, 0xd3, 0xae, 0x66, 0x48, 0x6e, 0x62, 0xfe, 0x5e, 0x82, 0x52, 0x06, - 0xa2, 0x16, 0x6c, 0x8c, 0x23, 0x5f, 0x7b, 0xdc, 0x6a, 0x3d, 0xff, 0x74, 0x5b, 0xfe, 0xdb, 0x8e, - 0x7c, 0x4e, 0x94, 0x2d, 0x6a, 0xc1, 0x7e, 0xe6, 0x8a, 0xa6, 0xd1, 0x22, 0x19, 0x73, 0x1a, 0x2f, - 0xae, 0x6e, 0xf8, 0x6d, 0x76, 0xdb, 0xbb, 0x19, 0xe9, 0x29, 0x6e, 0xa0, 0x28, 0xf4, 0x1d, 0x6c, - 0xe5, 0xad, 0xb6, 0x88, 0x7d, 0x26, 0xb8, 0xba, 0xfb, 0x6a, 0xab, 0xb1, 0x12, 0x31, 0xeb, 0xb8, - 0x91, 0xe2, 0x49, 0x7d, 0xbc, 0xba, 0x94, 0x6d, 0x35, 0x15, 0xb3, 0xb1, 0xbe, 0x3d, 0xd9, 0xd7, - 0x1b, 0xa4, 0x2c, 0x01, 0x75, 0x6f, 0x26, 0xd4, 0xa3, 0x30, 0x88, 0x42, 0x9a, 0x4e, 0x19, 0x6d, - 0xbd, 0x7e, 0xa3, 0x7a, 0xb9, 0x46, 0xaa, 0x0a, 0xf4, 0xa6, 0xac, 0xf5, 0xfa, 0x8d, 0x6c, 0x3d, - 0x35, 0x3d, 0xfc, 0xb7, 0x38, 0x48, 0x6e, 0x1b, 0xc5, 0xe3, 0xc2, 0x49, 0x9d, 0xa8, 0x81, 0xc2, - 0x0a, 0x41, 0x7b, 0xb0, 0x79, 0x3d, 0x63, 0x93, 0xb4, 0x51, 0x52, 0x94, 0x5e, 0x98, 0x7f, 0x6f, - 0x40, 0x75, 0xa5, 0x04, 0xa8, 0x06, 0x65, 0x82, 0x3d, 0x4c, 0x2e, 0x70, 0xc7, 0xf8, 0x0f, 0x6a, - 0xc0, 0xde, 0xc8, 0x79, 0xe7, 0xb8, 0xef, 0x1d, 0x3a, 0xb0, 0x2e, 0xfb, 0xd8, 0x19, 0xd2, 0x73, - 0xcb, 0x3b, 0x37, 0x0a, 0xe8, 0x19, 0x34, 0x6c, 0xa7, 0xed, 0x12, 0x82, 0xdb, 0xc3, 0x25, 0x67, - 0xf5, 0xdd, 0x91, 0x33, 0x34, 0xd6, 0xd0, 0x0b, 0x38, 0xec, 0xda, 0x8e, 0xd5, 0xa3, 0x77, 0x36, - 0xed, 0xde, 0xf0, 0x82, 0xe2, 0x0f, 0x03, 0x9b, 0x5c, 0x1a, 0xeb, 0x8f, 0x19, 0x9c, 0x0f, 0x7b, - 0xed, 0xdc, 0xc3, 0x06, 0x3a, 0x80, 0x7d, 0x6d, 0xa0, 0xb7, 0xd0, 0xa1, 0xeb, 0x52, 0xcf, 0x75, - 0x1d, 0x63, 0x13, 0xed, 0x40, 0xdd, 0x76, 0x2e, 0xac, 0x9e, 0xdd, 0xa1, 0x04, 0x5b, 0xbd, 0xbe, - 0x51, 0x44, 0xbb, 0xb0, 0xfd, 0xd0, 0xae, 0x24, 0x5d, 0xe4, 0x76, 0xae, 0x63, 0xbb, 0x0e, 0xbd, - 0xc0, 0xc4, 0xb3, 0x5d, 0xc7, 0x28, 0xa3, 0x27, 0x80, 0xee, 0x53, 0xe7, 0x7d, 0xab, 0x6d, 0x54, - 0xd0, 0x3e, 0xec, 0xdc, 0xc7, 0xdf, 0xe1, 0x4b, 0x03, 0x64, 0x19, 0xf4, 0xc1, 0xe8, 0x5b, 0xdc, - 0x73, 0xdf, 0xd3, 0xbe, 0xed, 0xd8, 0xfd, 0x51, 0xdf, 0xa8, 0xa2, 0x3d, 0x30, 0xba, 0x18, 0x53, - 0xdb, 0xf1, 0x46, 0xdd, 0xae, 0xdd, 0xb6, 0xb1, 0x33, 0x34, 0x6a, 0x3a, 0xf2, 0x63, 0x89, 0xd7, - 0xe5, 0x86, 0xf6, 0xb9, 0xe5, 0x38, 0xb8, 0x47, 0x3b, 0xb6, 0x67, 0xbd, 0xed, 0xe1, 0x8e, 0xb1, - 0x85, 0x8e, 0xe0, 0x60, 0x88, 0xfb, 0x03, 0x97, 0x58, 0xe4, 0x92, 0xe6, 0x7c, 0xd7, 0xb2, 0x7b, - 0x23, 0x82, 0x8d, 0x6d, 0xf4, 0x12, 0x8e, 0x08, 0xfe, 0x61, 0x64, 0x13, 0xdc, 0xa1, 0x8e, 0xdb, - 0xc1, 0xb4, 0x8b, 0xad, 0xe1, 0x88, 0x60, 0xda, 0xb7, 0x3d, 0xcf, 0x76, 0xbe, 0x37, 0x0c, 0xf4, - 0x3f, 0x38, 0x5e, 0x9a, 0x2c, 0x1d, 0x3c, 0xb0, 0xda, 0x91, 0xf9, 0xe5, 0xf7, 0xe9, 0xe0, 0x0f, - 0x43, 0x3a, 0xc0, 0x98, 0x18, 0x08, 0x35, 0xe1, 0xc9, 0x5d, 0x78, 0x1d, 0x20, 0x8b, 0xbd, 0x2b, - 0xb9, 0x01, 0x26, 0x7d, 0xcb, 0x91, 0x17, 0x7c, 0x8f, 0xdb, 0x93, 0xc7, 0xbe, 0xe3, 0x1e, 0x1e, - 0x7b, 0xdf, 0xfc, 0x63, 0x0d, 0xea, 0xf7, 0x9a, 0x1e, 0x3d, 0x83, 0x4a, 0x1a, 0x4c, 0x42, 0x26, - 0xe4, 0x28, 0xeb, 0x29, 0xbf, 0x03, 0xd4, 0x03, 0x30, 0x65, 0x41, 0xa8, 0xe5, 0x45, 0x4f, 0x5b, - 0x45, 0x21, 0x4a, 0x5c, 0x9e, 0x42, 0x49, 0xce, 0x8c, 0xd4, 0xf2, 0x75, 0x35, 0x20, 0x45, 0xb9, - 0xb4, 0x7d, 0xe9, 0x55, 0xea, 0x57, 0x2a, 0xd8, 0x3c, 0x56, 0xb3, 0x53, 0x27, 0x77, 0x00, 0xfa, - 0x2f, 0xe4, 0xa3, 0x46, 0x75, 0xff, 0x6f, 0x2a, 0x8b, 0x5a, 0x06, 0x76, 0x25, 0xf6, 0x89, 0x32, - 0x0a, 0x96, 0x4d, 0xd0, 0xaa, 0x32, 0x0a, 0x86, 0xbe, 0x84, 0x1d, 0x3d, 0xa6, 0x41, 0x18, 0xcc, - 0x17, 0x73, 0x3d, 0xae, 0x25, 0x75, 0x9a, 0x6d, 0x35, 0xae, 0x1a, 0x57, 0x53, 0x7b, 0x00, 0xe5, - 0x2b, 0x96, 0x72, 0x29, 0xca, 0x8d, 0xb2, 0x72, 0x56, 0x92, 0xeb, 0x2e, 0x57, 0xef, 0x8b, 0x94, - 0xea, 0x44, 0x0a, 0x45, 0x45, 0x53, 0xd7, 0x9c, 0x13, 0x26, 0xb8, 0xf9, 0x0c, 0x9a, 0x84, 0xa7, - 0x5c, 0xf4, 0x83, 0x34, 0x0d, 0xa2, 0xb0, 0x1d, 0x85, 0x22, 0x89, 0x66, 0x99, 0x06, 0x9b, 0x47, - 0x70, 0xf8, 0x28, 0xab, 0x45, 0xb4, 0xf5, 0xd7, 0x1a, 0x14, 0x95, 0xac, 0x26, 0xa8, 0x03, 0x55, - 0x29, 0xb3, 0xd9, 0xcb, 0x86, 0x0e, 0x56, 0x84, 0xe8, 0xfe, 0xa3, 0xdd, 0x6c, 0x3e, 0x46, 0x65, - 0xaa, 0xfc, 0x0e, 0x0c, 0x9c, 0x8a, 0x60, 0x2e, 0x15, 0x2b, 0x7b, 0x77, 0xd0, 0xaa, 0xfd, 0x83, - 0xc7, 0xac, 0x79, 0xf8, 0x28, 0x97, 0x39, 0xeb, 0xe9, 0x23, 0x65, 0xca, 0x8f, 0x8e, 0x56, 0x6c, - 0x3f, 0x7d, 0x6e, 0x9a, 0xcf, 0x3f, 0x47, 0x67, 0xde, 0x7c, 0xd8, 0x7d, 0xa4, 0x14, 0xe8, 0xff, - 0xab, 0x27, 0xf8, 0x6c, 0x21, 0x9b, 0xaf, 0xfe, 0xcd, 0x4c, 0x47, 0x79, 0xfb, 0xf5, 0x8f, 0x67, - 0x93, 0x40, 0x4c, 0x17, 0x57, 0xa7, 0xe3, 0x68, 0x7e, 0x36, 0x0b, 0x26, 0x53, 0x11, 0x06, 0xe1, - 0x24, 0xe4, 0xe2, 0xd7, 0x28, 0xb9, 0x39, 0x9b, 0x85, 0xfe, 0x99, 0x7a, 0xe3, 0xce, 0x96, 0xee, - 0xae, 0x8a, 0xea, 0xef, 0xd1, 0x37, 0xff, 0x04, 0x00, 0x00, 0xff, 0xff, 0x3c, 0xfc, 0x5a, 0x02, - 0x4e, 0x09, 0x00, 0x00, + // 1423 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x56, 0xd9, 0x72, 0x1a, 0x47, + 0x17, 0xfe, 0xd1, 0xc2, 0x72, 0x58, 0x34, 0x6a, 0x2d, 0x46, 0xc8, 0xb2, 0xe5, 0xf9, 0xfd, 0xfb, + 0x57, 0xa5, 0x5c, 0x52, 0x85, 0x94, 0x7c, 0x99, 0x14, 0x86, 0x21, 0x4c, 0x09, 0x06, 0xdc, 0x80, + 0x6c, 0x25, 0x17, 0x5d, 0xa3, 0x99, 0x16, 0x4c, 0xc4, 0x2c, 0x9e, 0x19, 0x92, 0x90, 0x07, 0xc8, + 0x83, 0xe4, 0x11, 0x52, 0x95, 0x8b, 0x3c, 0x40, 0x1e, 0x22, 0x6f, 0x93, 0xea, 0x65, 0x58, 0x24, + 0x9c, 0xe4, 0x0a, 0xe6, 0xfb, 0x4e, 0x9f, 0xd3, 0x67, 0xed, 0x03, 0x87, 0xa1, 0x3f, 0x8d, 0x69, + 0x18, 0x06, 0xd6, 0x85, 0xf8, 0x77, 0x1e, 0x84, 0x7e, 0xec, 0xa3, 0xdc, 0x1c, 0xaf, 0xe4, 0xc2, + 0xc0, 0x12, 0xa8, 0xfa, 0x47, 0x0a, 0x4a, 0x3d, 0x73, 0xe6, 0x52, 0x2f, 0xc6, 0xf4, 0xe3, 0x94, + 0x46, 0x31, 0x7a, 0x02, 0x99, 0xc0, 0x9c, 0x91, 0x90, 0x7e, 0x2c, 0xa7, 0x4e, 0x53, 0x67, 0x39, + 0x9c, 0x0e, 0xcc, 0x19, 0xa6, 0x1f, 0x91, 0x0a, 0xc5, 0x3b, 0x4a, 0xc9, 0xc4, 0x71, 0x9d, 0x98, + 0x44, 0x66, 0x5c, 0xde, 0x38, 0x4d, 0x9d, 0x6d, 0xe2, 0xfc, 0x1d, 0xa5, 0x6d, 0x86, 0xf5, 0xcd, + 0x18, 0x9d, 0x00, 0x58, 0x93, 0xf8, 0x7b, 0x21, 0x54, 0xde, 0x3c, 0x4d, 0x9d, 0x6d, 0xe3, 0x1c, + 0x43, 0xb8, 0x04, 0xfa, 0x3f, 0xec, 0xc4, 0x8e, 0x4b, 0xfd, 0x69, 0x4c, 0x22, 0x6a, 0xf9, 0x9e, + 0x1d, 0x95, 0xb7, 0xb8, 0x4c, 0x49, 0xc2, 0x7d, 0x81, 0xa2, 0x73, 0xd8, 0xf3, 0xa7, 0xf1, 0xc8, + 0x77, 0xbc, 0x11, 0xb1, 0xc6, 0xa6, 0xe7, 0xd1, 0x09, 0x71, 0xec, 0xf2, 0x36, 0xb7, 0xb8, 0x9b, + 0x50, 0x75, 0xc1, 0xe8, 0xb6, 0xfa, 0x1d, 0xec, 0xcc, 0xdd, 0x88, 0x02, 0xdf, 0x8b, 0x28, 0x3a, + 0x82, 0x2c, 0xf3, 0x63, 0x6c, 0x46, 0x63, 0xee, 0x48, 0x01, 0x33, 0xbf, 0x5a, 0x66, 0x34, 0x46, + 0xc7, 0x90, 0x0b, 0x42, 0x4a, 0x1c, 0xd7, 0x1c, 0x51, 0xee, 0x45, 0x01, 0x67, 0x83, 0x90, 0xea, + 0xec, 0x1b, 0x3d, 0x87, 0x7c, 0x20, 0x54, 0x11, 0x1a, 0x86, 0xdc, 0x87, 0x1c, 0x06, 0x09, 0x69, + 0x61, 0xa8, 0x7e, 0x09, 0x3b, 0x98, 0xc5, 0xb2, 0x49, 0x69, 0x12, 0x33, 0x04, 0x5b, 0x36, 0x8d, + 0x62, 0x69, 0x87, 0xff, 0x67, 0x71, 0x34, 0xdd, 0xe5, 0x40, 0xa5, 0x4d, 0x97, 0xc5, 0x48, 0xb5, + 0x41, 0x59, 0x9c, 0x97, 0x97, 0x3d, 0x03, 0x85, 0xe5, 0x87, 0xb9, 0xcb, 0x62, 0xec, 0xb2, 0x53, + 0x29, 0x7e, 0xaa, 0x24, 0xf1, 0x26, 0xa5, 0x9d, 0xc8, 0x8c, 0xd1, 0x2b, 0x11, 0x42, 0x32, 0xf1, + 0xad, 0x7b, 0x62, 0xd3, 0x89, 0x39, 0x93, 0xea, 0x8b, 0x0c, 0x6e, 0xfb, 0xd6, 0x7d, 0x83, 0x81, + 0xea, 0xb7, 0x80, 0xfa, 0xd4, 0xb3, 0x07, 0x3e, 0xb7, 0x95, 0x5c, 0xf4, 0x05, 0x14, 0x12, 0xe7, + 0x96, 0x02, 0x93, 0x38, 0xcc, 0x83, 0xa3, 0xc2, 0x36, 0x2f, 0x15, 0xae, 0x36, 0x5f, 0x2d, 0x9c, + 0x4f, 0x3c, 0x56, 0x2f, 0x42, 0x8d, 0xa0, 0x54, 0x02, 0x7b, 0x2b, 0xca, 0xa5, 0x17, 0x15, 0x60, + 0x61, 0x14, 0x61, 0x4d, 0xcd, 0xc3, 0xca, 0xbf, 0xd1, 0x6b, 0xc8, 0xdc, 0x99, 0xce, 0x64, 0x1a, + 0x26, 0x8a, 0xd1, 0xf9, 0xbc, 0x22, 0xcf, 0x9b, 0x82, 0xc1, 0x89, 0x88, 0xfa, 0x73, 0x06, 0x32, + 0x12, 0x44, 0x55, 0xd8, 0xb2, 0x7c, 0x5b, 0x68, 0x2c, 0x55, 0x9f, 0x3d, 0x3e, 0x96, 0xfc, 0xd6, + 0x7d, 0x9b, 0x62, 0x2e, 0x8b, 0xaa, 0x70, 0x20, 0x55, 0x91, 0xc8, 0x9f, 0x86, 0x16, 0x25, 0xc1, + 0xf4, 0xf6, 0x9e, 0xce, 0x64, 0xb6, 0xf7, 0x24, 0xd9, 0xe7, 0x5c, 0x8f, 0x53, 0xe8, 0x2b, 0x28, + 0x25, 0xa5, 0x36, 0x0d, 0x6c, 0x33, 0xa6, 0x3c, 0xf7, 0xf9, 0x6a, 0x79, 0xc9, 0xa2, 0xac, 0xb8, + 0x21, 0xe7, 0x71, 0xd1, 0x5a, 0xfe, 0x64, 0x65, 0x35, 0x8e, 0x27, 0x96, 0xc8, 0x1e, 0xab, 0xeb, + 0x2d, 0x9c, 0x65, 0x00, 0xcf, 0x9b, 0x0a, 0x45, 0xdf, 0x73, 0x7c, 0x8f, 0x44, 0x63, 0x93, 0x54, + 0x2f, 0xdf, 0xf0, 0x5a, 0x2e, 0xe0, 0x3c, 0x07, 0xfb, 0x63, 0xb3, 0x7a, 0xf9, 0x86, 0x95, 0x1e, + 0xef, 0x1e, 0xfa, 0x63, 0xe0, 0x84, 0xb3, 0x72, 0xfa, 0x34, 0x75, 0x56, 0xc4, 0xbc, 0xa1, 0x34, + 0x8e, 0xa0, 0x7d, 0xd8, 0xbe, 0x9b, 0x98, 0xa3, 0xa8, 0x9c, 0xe1, 0x94, 0xf8, 0x50, 0xff, 0xdc, + 0x82, 0xfc, 0x52, 0x08, 0x50, 0x01, 0xb2, 0x58, 0xeb, 0x6b, 0xf8, 0x5a, 0x6b, 0x28, 0xff, 0x41, + 0x65, 0xd8, 0x1f, 0x1a, 0x57, 0x46, 0xf7, 0xbd, 0x41, 0x7a, 0xb5, 0x9b, 0x8e, 0x66, 0x0c, 0x48, + 0xab, 0xd6, 0x6f, 0x29, 0x29, 0xf4, 0x14, 0xca, 0xba, 0x51, 0xef, 0x62, 0xac, 0xd5, 0x07, 0x73, + 0xae, 0xd6, 0xe9, 0x0e, 0x8d, 0x81, 0xb2, 0x81, 0x9e, 0xc3, 0x71, 0x53, 0x37, 0x6a, 0x6d, 0xb2, + 0x90, 0xa9, 0xb7, 0x07, 0xd7, 0x44, 0xfb, 0xd0, 0xd3, 0xf1, 0x8d, 0xb2, 0xb9, 0x4e, 0xa0, 0x35, + 0x68, 0xd7, 0x13, 0x0d, 0x5b, 0xe8, 0x08, 0x0e, 0x84, 0x80, 0x38, 0x42, 0x06, 0xdd, 0x2e, 0xe9, + 0x77, 0xbb, 0x86, 0xb2, 0x8d, 0x76, 0xa1, 0xa8, 0x1b, 0xd7, 0xb5, 0xb6, 0xde, 0x20, 0x58, 0xab, + 0xb5, 0x3b, 0x4a, 0x1a, 0xed, 0xc1, 0xce, 0x43, 0xb9, 0x0c, 0x53, 0x91, 0xc8, 0x75, 0x0d, 0xbd, + 0x6b, 0x90, 0x6b, 0x0d, 0xf7, 0xf5, 0xae, 0xa1, 0x64, 0xd1, 0x21, 0xa0, 0x55, 0xaa, 0xd5, 0xa9, + 0xd5, 0x95, 0x1c, 0x3a, 0x80, 0xdd, 0x55, 0xfc, 0x4a, 0xbb, 0x51, 0x80, 0x85, 0x41, 0x5c, 0x8c, + 0xbc, 0xd5, 0xda, 0xdd, 0xf7, 0xa4, 0xa3, 0x1b, 0x7a, 0x67, 0xd8, 0x51, 0xf2, 0x68, 0x1f, 0x94, + 0xa6, 0xa6, 0x11, 0xdd, 0xe8, 0x0f, 0x9b, 0x4d, 0xbd, 0xae, 0x6b, 0xc6, 0x40, 0x29, 0x08, 0xcb, + 0xeb, 0x1c, 0x2f, 0xb2, 0x03, 0xf5, 0x56, 0xcd, 0x30, 0xb4, 0x36, 0x69, 0xe8, 0xfd, 0xda, 0xdb, + 0xb6, 0xd6, 0x50, 0x4a, 0xe8, 0x04, 0x8e, 0x06, 0x5a, 0xa7, 0xd7, 0xc5, 0x35, 0x7c, 0x43, 0x12, + 0xbe, 0x59, 0xd3, 0xdb, 0x43, 0xac, 0x29, 0x3b, 0xe8, 0x05, 0x9c, 0x60, 0xed, 0xdd, 0x50, 0xc7, + 0x5a, 0x83, 0x18, 0xdd, 0x86, 0x46, 0x9a, 0x5a, 0x6d, 0x30, 0xc4, 0x1a, 0xe9, 0xe8, 0xfd, 0xbe, + 0x6e, 0x7c, 0xad, 0x28, 0xe8, 0x25, 0x9c, 0xce, 0x45, 0xe6, 0x0a, 0x1e, 0x48, 0xed, 0x32, 0xff, + 0x92, 0x7c, 0x1a, 0xda, 0x87, 0x01, 0xe9, 0x69, 0x1a, 0x56, 0x10, 0xaa, 0xc0, 0xe1, 0xc2, 0xbc, + 0x30, 0x20, 0x6d, 0xef, 0x31, 0xae, 0xa7, 0xe1, 0x4e, 0xcd, 0x60, 0x09, 0x5e, 0xe1, 0xf6, 0xd9, + 0xb5, 0x17, 0xdc, 0xc3, 0x6b, 0x1f, 0xa8, 0xbf, 0x6e, 0x40, 0x71, 0xa5, 0xe8, 0xd1, 0x53, 0xc8, + 0x45, 0xce, 0xc8, 0x33, 0x63, 0xd6, 0xca, 0xa2, 0xcb, 0x17, 0x00, 0x7f, 0x00, 0xc6, 0xa6, 0xe3, + 0x89, 0xf1, 0x22, 0xba, 0x2d, 0xc7, 0x11, 0x3e, 0x5c, 0x9e, 0x40, 0x86, 0xf5, 0x0c, 0x9b, 0xe5, + 0x9b, 0xbc, 0x41, 0xd2, 0xec, 0x53, 0xb7, 0x99, 0x56, 0x36, 0xbf, 0xa2, 0xd8, 0x74, 0x03, 0xde, + 0x3b, 0x45, 0xbc, 0x00, 0xd0, 0x7f, 0x21, 0x69, 0x35, 0x22, 0xea, 0x7f, 0x9b, 0x4b, 0x14, 0x24, + 0xd8, 0x64, 0xd8, 0xa3, 0xc9, 0x18, 0x9b, 0xb2, 0x83, 0x96, 0x27, 0x63, 0x6c, 0xa2, 0xcf, 0x60, + 0x57, 0xb4, 0xa9, 0xe3, 0x39, 0xee, 0xd4, 0x15, 0xed, 0x9a, 0xe1, 0xb7, 0xd9, 0xe1, 0xed, 0x2a, + 0x70, 0xde, 0xb5, 0x47, 0x90, 0xbd, 0x35, 0x23, 0xca, 0x86, 0x72, 0x39, 0xcb, 0x95, 0x65, 0xd8, + 0x77, 0x93, 0xf2, 0xf7, 0x85, 0x8d, 0xea, 0x90, 0x0d, 0x8a, 0x9c, 0xa0, 0xee, 0x28, 0xc5, 0x66, + 0x4c, 0xd5, 0xa7, 0x50, 0xc1, 0x34, 0xa2, 0x71, 0xc7, 0x89, 0x22, 0xc7, 0xf7, 0xea, 0xbe, 0x17, + 0x87, 0xfe, 0x44, 0xce, 0x60, 0xf5, 0x04, 0x8e, 0xd7, 0xb2, 0x62, 0x88, 0xb2, 0xc3, 0xef, 0xa6, + 0x34, 0x9c, 0xad, 0x3f, 0x7c, 0x05, 0xc7, 0x6b, 0x59, 0x39, 0x81, 0x5f, 0xc3, 0xb6, 0xe7, 0xdb, + 0x34, 0x2a, 0xa7, 0x4e, 0x37, 0xcf, 0xf2, 0xd5, 0xc3, 0xa5, 0xd1, 0x65, 0xf8, 0x36, 0x6d, 0x39, + 0x51, 0xec, 0x87, 0x33, 0x2c, 0x84, 0xd4, 0xdf, 0x53, 0x90, 0x5f, 0x82, 0xd1, 0x21, 0xa4, 0xe5, + 0x98, 0x14, 0x79, 0x95, 0x5f, 0xe8, 0x25, 0x94, 0x26, 0x66, 0x14, 0x13, 0x36, 0x35, 0x09, 0x0b, + 0xa6, 0x7c, 0x72, 0x0a, 0x0c, 0x65, 0x93, 0x67, 0xe0, 0xb8, 0x14, 0x5d, 0xc2, 0x13, 0x3f, 0x1e, + 0xd3, 0x90, 0x3f, 0xd8, 0x24, 0x9a, 0x5a, 0x16, 0x8d, 0x22, 0x12, 0x84, 0xfe, 0x2d, 0xcf, 0xf5, + 0x06, 0xde, 0xe7, 0x34, 0xab, 0xa6, 0xbe, 0x20, 0x7b, 0xa1, 0x7f, 0x8b, 0x2e, 0x21, 0x2b, 0xd3, + 0xc8, 0x96, 0x01, 0x76, 0xeb, 0xa3, 0xc7, 0x03, 0x37, 0xb9, 0xf8, 0x5c, 0x54, 0xfd, 0x25, 0x05, + 0xa5, 0x55, 0x52, 0xd6, 0x5e, 0xb2, 0x2b, 0xa4, 0x78, 0x46, 0x73, 0x56, 0xb2, 0x23, 0xfc, 0x4b, + 0x2f, 0xce, 0x40, 0x71, 0x1d, 0x8f, 0x04, 0xd4, 0x33, 0x27, 0xce, 0x4f, 0x94, 0x98, 0xae, 0xd8, + 0x63, 0x36, 0x71, 0xc9, 0x75, 0xbc, 0x9e, 0x84, 0x6b, 0x2e, 0x7f, 0x4b, 0x57, 0x9c, 0xdc, 0xe2, + 0x4e, 0xe6, 0xa3, 0x85, 0x6f, 0xd5, 0xdf, 0x36, 0x21, 0xcd, 0x9f, 0xc8, 0x10, 0x35, 0x20, 0xcf, + 0x9e, 0x4c, 0xb9, 0xa5, 0xa0, 0x65, 0x1f, 0x57, 0x17, 0xb0, 0x4a, 0x65, 0x1d, 0x25, 0xf3, 0x7b, + 0x05, 0x8a, 0x16, 0xc5, 0x8e, 0xcb, 0x5e, 0x1f, 0xb9, 0x43, 0xa0, 0x65, 0xf9, 0x07, 0x8b, 0x49, + 0xe5, 0x78, 0x2d, 0x27, 0x95, 0xb5, 0xc5, 0x95, 0xe4, 0x2b, 0x8e, 0x4e, 0x96, 0x64, 0x1f, 0xaf, + 0x0e, 0x95, 0x67, 0x9f, 0xa2, 0xa5, 0x36, 0x1b, 0xf6, 0xd6, 0x94, 0x35, 0xfa, 0xdf, 0xf2, 0x0d, + 0x3e, 0xd9, 0x14, 0x95, 0x57, 0xff, 0x24, 0xb6, 0xb0, 0xb2, 0xa6, 0xfe, 0x57, 0xac, 0x7c, 0xba, + 0x7b, 0x56, 0xac, 0xfc, 0x4d, 0x1b, 0xbd, 0xfd, 0xfc, 0x9b, 0x8b, 0x91, 0x13, 0x8f, 0xa7, 0xb7, + 0xe7, 0x96, 0xef, 0x5e, 0x4c, 0x9c, 0xd1, 0x38, 0xf6, 0x1c, 0x6f, 0xe4, 0xd1, 0xf8, 0x07, 0x3f, + 0xbc, 0xbf, 0x98, 0x78, 0xf6, 0x05, 0xdf, 0x8a, 0x2e, 0xe6, 0xea, 0x6e, 0xd3, 0x7c, 0xa1, 0xfe, + 0xe2, 0xaf, 0x00, 0x00, 0x00, 0xff, 0xff, 0x22, 0x80, 0x43, 0x06, 0x80, 0x0b, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -886,6 +1109,10 @@ type RouterClient interface { //ResetMissionControl clears all mission control state and starts with a clean //slate. ResetMissionControl(ctx context.Context, in *ResetMissionControlRequest, opts ...grpc.CallOption) (*ResetMissionControlResponse, error) + //* + //QueryMissionControl exposes the internal mission control state to callers. + //It is a development feature. + QueryMissionControl(ctx context.Context, in *QueryMissionControlRequest, opts ...grpc.CallOption) (*QueryMissionControlResponse, error) } type routerClient struct { @@ -932,6 +1159,15 @@ func (c *routerClient) ResetMissionControl(ctx context.Context, in *ResetMission return out, nil } +func (c *routerClient) QueryMissionControl(ctx context.Context, in *QueryMissionControlRequest, opts ...grpc.CallOption) (*QueryMissionControlResponse, error) { + out := new(QueryMissionControlResponse) + err := c.cc.Invoke(ctx, "/routerrpc.Router/QueryMissionControl", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // RouterServer is the server API for Router service. type RouterServer interface { //* @@ -954,6 +1190,10 @@ type RouterServer interface { //ResetMissionControl clears all mission control state and starts with a clean //slate. ResetMissionControl(context.Context, *ResetMissionControlRequest) (*ResetMissionControlResponse, error) + //* + //QueryMissionControl exposes the internal mission control state to callers. + //It is a development feature. + QueryMissionControl(context.Context, *QueryMissionControlRequest) (*QueryMissionControlResponse, error) } func RegisterRouterServer(s *grpc.Server, srv RouterServer) { @@ -1032,6 +1272,24 @@ func _Router_ResetMissionControl_Handler(srv interface{}, ctx context.Context, d return interceptor(ctx, in, info, handler) } +func _Router_QueryMissionControl_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(QueryMissionControlRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RouterServer).QueryMissionControl(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/routerrpc.Router/QueryMissionControl", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RouterServer).QueryMissionControl(ctx, req.(*QueryMissionControlRequest)) + } + return interceptor(ctx, in, info, handler) +} + var _Router_serviceDesc = grpc.ServiceDesc{ ServiceName: "routerrpc.Router", HandlerType: (*RouterServer)(nil), @@ -1052,6 +1310,10 @@ var _Router_serviceDesc = grpc.ServiceDesc{ MethodName: "ResetMissionControl", Handler: _Router_ResetMissionControl_Handler, }, + { + MethodName: "QueryMissionControl", + Handler: _Router_QueryMissionControl_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "routerrpc/router.proto", diff --git a/lnrpc/routerrpc/router.proto b/lnrpc/routerrpc/router.proto index f9262e81e7fe..6636162538e7 100644 --- a/lnrpc/routerrpc/router.proto +++ b/lnrpc/routerrpc/router.proto @@ -213,6 +213,43 @@ message ResetMissionControlRequest{} message ResetMissionControlResponse{} +message QueryMissionControlRequest {} + +/// QueryMissionControlResponse contains mission control state per node. +message QueryMissionControlResponse { + repeated NodeHistory nodes = 1; +} + +/// NodeHistory contains the mission control state for a particular node. +message NodeHistory { + /// Node pubkey + bytes pubkey = 1; + + /// Time stamp of last failure. Set to zero if no failure happened yet. + int64 last_fail_time = 2; + + /// Estimation of success probability for channels not in the channel array. + float other_chan_success_prob = 3; + + /// Historical information of particular channels. + repeated ChannelHistory channels = 4; +} + +/// NodeHistory contains the mission control state for a particular channel. +message ChannelHistory { + /// Short channel id + uint64 channel_id = 1; + + /// Time stamp of last failure. + int64 last_fail_time = 2; + + /// Minimum penalization amount. + int64 min_penalize_amt = 3; + + /// Estimation of success probability for this channel. + float success_prob = 4; +} + service Router { /** SendPayment attempts to route a payment described by the passed @@ -241,4 +278,10 @@ service Router { slate. */ rpc ResetMissionControl(ResetMissionControlRequest) returns (ResetMissionControlResponse); + + /** + QueryMissionControl exposes the internal mission control state to callers. + It is a development feature. + */ + rpc QueryMissionControl(QueryMissionControlRequest) returns (QueryMissionControlResponse); } diff --git a/lnrpc/routerrpc/router_server.go b/lnrpc/routerrpc/router_server.go index 0cec2c8faee2..cf5722069c4f 100644 --- a/lnrpc/routerrpc/router_server.go +++ b/lnrpc/routerrpc/router_server.go @@ -59,6 +59,10 @@ var ( Entity: "offchain", Action: "read", }}, + "/routerrpc.Router/QueryMissionControl": {{ + Entity: "offchain", + Action: "read", + }}, "/routerrpc.Router/ResetMissionControl": {{ Entity: "offchain", Action: "write", @@ -450,3 +454,48 @@ func (s *Server) ResetMissionControl(ctx context.Context, return &ResetMissionControlResponse{}, nil } + +// QueryMissionControl exposes the internal mission control state to callers. It +// is a development feature. +func (s *Server) QueryMissionControl(ctx context.Context, + req *QueryMissionControlRequest) (*QueryMissionControlResponse, error) { + + snapshot := s.cfg.RouterBackend.MissionControl.GetHistorySnapshot() + + rpcNodes := make([]*NodeHistory, len(snapshot.Nodes)) + for i, node := range snapshot.Nodes { + channels := make([]*ChannelHistory, len(node.Channels)) + for j, channel := range node.Channels { + channels[j] = &ChannelHistory{ + ChannelId: channel.ChannelID, + LastFailTime: channel.LastFail.Unix(), + MinPenalizeAmt: int64( + channel.MinPenalizeAmt.ToSatoshis(), + ), + SuccessProb: float32(channel.SuccessProb), + } + } + + var lastFail int64 + if node.LastFail != nil { + lastFail = node.LastFail.Unix() + } + + rpcNode := NodeHistory{ + Pubkey: node.Node[:], + LastFailTime: lastFail, + OtherChanSuccessProb: float32( + node.OtherChanSuccessProb, + ), + Channels: channels, + } + + rpcNodes[i] = &rpcNode + } + + response := QueryMissionControlResponse{ + Nodes: rpcNodes, + } + + return &response, nil +} diff --git a/routing/missioncontrol.go b/routing/missioncontrol.go index 760703200fd9..546a64a3e205 100644 --- a/routing/missioncontrol.go +++ b/routing/missioncontrol.go @@ -76,6 +76,48 @@ type channelHistory struct { minPenalizeAmt lnwire.MilliSatoshi } +// MissionControlSnapshot contains a snapshot of the current state of mission +// control. +type MissionControlSnapshot struct { + // Nodes contains the per node information of this snapshot. + Nodes []MissionControlNodeSnapshot +} + +// MissionControlNodeSnapshot contains a snapshot of the current node state in +// mission control. +type MissionControlNodeSnapshot struct { + // Node pubkey. + Node route.Vertex + + // Lastfail is the time of last failure, if any. + LastFail *time.Time + + // Channels is a list of channels for which specific information is + // logged. + Channels []MissionControlChannelSnapshot + + // OtherChanSuccessProb is the success probability for channels not in + // the Channels slice. + OtherChanSuccessProb float64 +} + +// MissionControlChannelSnapshot contains a snapshot of the current channel +// state in mission control. +type MissionControlChannelSnapshot struct { + // ChannelID is the short channel id of the snapshot. + ChannelID uint64 + + // LastFail is the time of last failure. + LastFail time.Time + + // MinPenalizeAmt is the minimum amount for which the channel will be + // penalized. + MinPenalizeAmt lnwire.MilliSatoshi + + // SuccessProb is the success probability estimation for this channel. + SuccessProb float64 +} + // NewMissionControl returns a new instance of missionControl. // // TODO(roasbeef): persist memory @@ -331,3 +373,55 @@ func (m *MissionControl) reportEdgeFailure(failedEdge edge, minPenalizeAmt: minPenalizeAmt, } } + +// GetHistorySnapshot takes a snapshot from the current mission control state +// and actual probability estimates. +func (m *MissionControl) GetHistorySnapshot() *MissionControlSnapshot { + m.Lock() + defer m.Unlock() + + log.Debugf("Requesting history snapshot from mission control: "+ + "node_count=%v", len(m.history)) + + nodes := make([]MissionControlNodeSnapshot, 0, len(m.history)) + + for v, h := range m.history { + channelSnapshot := make([]MissionControlChannelSnapshot, 0, + len(h.channelLastFail), + ) + + for id, lastFail := range h.channelLastFail { + // Show probability assuming amount meets min + // penalization amount. + prob := m.getEdgeProbabilityForNode( + h, id, lastFail.minPenalizeAmt, + ) + + channelSnapshot = append(channelSnapshot, + MissionControlChannelSnapshot{ + ChannelID: id, + LastFail: lastFail.lastFail, + MinPenalizeAmt: lastFail.minPenalizeAmt, + SuccessProb: prob, + }, + ) + } + + otherProb := m.getEdgeProbabilityForNode(h, 0, 0) + + nodes = append(nodes, + MissionControlNodeSnapshot{ + Node: v, + LastFail: h.lastFail, + OtherChanSuccessProb: otherProb, + Channels: channelSnapshot, + }, + ) + } + + snapshot := MissionControlSnapshot{ + Nodes: nodes, + } + + return &snapshot +} diff --git a/routing/missioncontrol_test.go b/routing/missioncontrol_test.go index 39ebb5bdd200..ed79c5f20383 100644 --- a/routing/missioncontrol_test.go +++ b/routing/missioncontrol_test.go @@ -64,4 +64,14 @@ func TestMissionControl(t *testing.T) { // to zero. mc.reportVertexFailure(testNode) expectP(1000, 0) + + // Check whether history snapshot looks sane. + history := mc.GetHistorySnapshot() + if len(history.Nodes) != 1 { + t.Fatal("unexpected number of nodes") + } + + if len(history.Nodes[0].Channels) != 1 { + t.Fatal("unexpected number of channels") + } } From 84762091094cfe411c6ccaeb7dd5d800a1b68084 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Fri, 10 May 2019 10:39:16 +0200 Subject: [PATCH 10/11] lncli: add querymc command Adds querymc command to lncli to dump mission control state. --- cmd/lncli/cmd_query_mission_control.go | 58 ++++++++++++++++++++++++++ cmd/lncli/main.go | 1 + cmd/lncli/routerrpc_active.go | 10 +++++ cmd/lncli/routerrpc_default.go | 10 +++++ 4 files changed, 79 insertions(+) create mode 100644 cmd/lncli/cmd_query_mission_control.go create mode 100644 cmd/lncli/routerrpc_active.go create mode 100644 cmd/lncli/routerrpc_default.go diff --git a/cmd/lncli/cmd_query_mission_control.go b/cmd/lncli/cmd_query_mission_control.go new file mode 100644 index 000000000000..ac93fe0434bf --- /dev/null +++ b/cmd/lncli/cmd_query_mission_control.go @@ -0,0 +1,58 @@ +// +build routerrpc + +package main + +import ( + "context" + "encoding/hex" + + "github.com/lightningnetwork/lnd/lnrpc/routerrpc" + + "github.com/urfave/cli" +) + +var queryMissionControlCommand = cli.Command{ + Name: "querymc", + Category: "Payments", + Action: actionDecorator(queryMissionControl), +} + +func queryMissionControl(ctx *cli.Context) error { + conn := getClientConn(ctx, false) + defer conn.Close() + + client := routerrpc.NewRouterClient(conn) + + req := &routerrpc.QueryMissionControlRequest{} + rpcCtx := context.Background() + snapshot, err := client.QueryMissionControl(rpcCtx, req) + if err != nil { + return err + } + + type displayNodeHistory struct { + Pubkey string + LastFailTime int64 + OtherChanSuccessProb float32 + Channels []*routerrpc.ChannelHistory + } + + displayResp := struct { + Nodes []displayNodeHistory + }{ + make([]displayNodeHistory, len(snapshot.Nodes)), + } + + for i, n := range snapshot.Nodes { + displayResp.Nodes[i] = displayNodeHistory{ + Pubkey: hex.EncodeToString(n.Pubkey), + LastFailTime: n.LastFailTime, + OtherChanSuccessProb: n.OtherChanSuccessProb, + Channels: n.Channels, + } + } + + printJSON(displayResp) + + return nil +} diff --git a/cmd/lncli/main.go b/cmd/lncli/main.go index f04d456894d8..73a6ce75722d 100644 --- a/cmd/lncli/main.go +++ b/cmd/lncli/main.go @@ -303,6 +303,7 @@ func main() { // Add any extra autopilot commands determined by build flags. app.Commands = append(app.Commands, autopilotCommands()...) app.Commands = append(app.Commands, invoicesCommands()...) + app.Commands = append(app.Commands, routerCommands()...) if err := app.Run(os.Args); err != nil { fatal(err) diff --git a/cmd/lncli/routerrpc_active.go b/cmd/lncli/routerrpc_active.go new file mode 100644 index 000000000000..4a34d6b1f3dc --- /dev/null +++ b/cmd/lncli/routerrpc_active.go @@ -0,0 +1,10 @@ +// +build routerrpc + +package main + +import "github.com/urfave/cli" + +// routerCommands will return nil for non-routerrpc builds. +func routerCommands() []cli.Command { + return []cli.Command{queryMissionControlCommand} +} diff --git a/cmd/lncli/routerrpc_default.go b/cmd/lncli/routerrpc_default.go new file mode 100644 index 000000000000..c2a5fe7bedd6 --- /dev/null +++ b/cmd/lncli/routerrpc_default.go @@ -0,0 +1,10 @@ +// +build !routerrpc + +package main + +import "github.com/urfave/cli" + +// routerCommands will return nil for non-routerrpc builds. +func routerCommands() []cli.Command { + return nil +} From 024b4fbf9225a759e329873db5cfce4c22b86dbe Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Wed, 22 May 2019 11:56:04 +0200 Subject: [PATCH 11/11] routing+routerrpc: expose mission control parameters in lnd config This commit exposes the three main parameters that influence mission control and path finding to the user as command line or config file flags. It allows for fine-tuning for optimal results. --- config.go | 4 +- lnrpc/routerrpc/config_active.go | 47 +++++++++++++++++++++++ lnrpc/routerrpc/config_default.go | 21 +++++++++- routing/missioncontrol.go | 64 ++++++++++++++++++++++--------- routing/missioncontrol_test.go | 16 +++++--- routing/pathfind.go | 8 +++- routing/payment_session.go | 4 +- routing/payment_session_test.go | 1 + routing/router_test.go | 6 +++ server.go | 7 ++++ 10 files changed, 148 insertions(+), 30 deletions(-) diff --git a/config.go b/config.go index 9ffc4fb2faea..a8c30e703409 100644 --- a/config.go +++ b/config.go @@ -27,6 +27,7 @@ import ( "github.com/lightningnetwork/lnd/discovery" "github.com/lightningnetwork/lnd/htlcswitch/hodl" "github.com/lightningnetwork/lnd/lncfg" + "github.com/lightningnetwork/lnd/lnrpc/routerrpc" "github.com/lightningnetwork/lnd/lnrpc/signrpc" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/routing" @@ -365,7 +366,8 @@ func loadConfig() (*config, error) { MinBackoff: defaultMinBackoff, MaxBackoff: defaultMaxBackoff, SubRPCServers: &subRPCServerConfigs{ - SignRPC: &signrpc.Config{}, + SignRPC: &signrpc.Config{}, + RouterRPC: routerrpc.DefaultConfig(), }, Autopilot: &autoPilotConfig{ MaxChannels: 5, diff --git a/lnrpc/routerrpc/config_active.go b/lnrpc/routerrpc/config_active.go index edf9803555af..c2ee3a23416d 100644 --- a/lnrpc/routerrpc/config_active.go +++ b/lnrpc/routerrpc/config_active.go @@ -3,7 +3,12 @@ package routerrpc import ( + "time" + + "github.com/lightningnetwork/lnd/lnwire" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcutil" "github.com/lightningnetwork/lnd/macaroons" "github.com/lightningnetwork/lnd/routing" ) @@ -19,6 +24,23 @@ type Config struct { // directory, named DefaultRouterMacFilename. RouterMacPath string `long:"routermacaroonpath" description:"Path to the router macaroon"` + // MinProbability is the minimum required route success probability to + // attempt the payment. + MinRouteProbability float64 `long:"minrtprob" description:"Minimum required route success probability to attempt the payment"` + + // AprioriHopProbability is the assumed success probability of a hop in + // a route when no other information is available. + AprioriHopProbability float64 `long:"apriorihopprob" description:"Assumed success probability of a hop in a route when no other information is available."` + + // PenaltyHalfLife defines after how much time a penalized node or + // channel is back at 50% probability. + PenaltyHalfLife time.Duration `long:"penaltyhalflife" description:"Defines the duration after which a penalized node or channel is back at 50% probability"` + + // AttemptCost is the virtual cost in path finding weight units of + // executing a payment attempt that fails. It is used to trade off + // potentially better routes against their probability of succeeding. + AttemptCost int64 `long:"attemptcost" description:"The (virtual) cost in sats of a failed payment attempt"` + // NetworkDir is the main network directory wherein the router rpc // server will find the macaroon named DefaultRouterMacFilename. NetworkDir string @@ -45,3 +67,28 @@ type Config struct { // main rpc server. RouterBackend *RouterBackend } + +// DefaultConfig defines the config defaults. +func DefaultConfig() *Config { + return &Config{ + AprioriHopProbability: routing.DefaultAprioriHopProbability, + MinRouteProbability: routing.DefaultMinRouteProbability, + PenaltyHalfLife: routing.DefaultPenaltyHalfLife, + AttemptCost: int64( + routing.DefaultPaymentAttemptPenalty.ToSatoshis(), + ), + } +} + +// GetMissionControlConfig returns the mission control config based on this sub +// server config. +func GetMissionControlConfig(cfg *Config) *routing.MissionControlConfig { + return &routing.MissionControlConfig{ + AprioriHopProbability: cfg.AprioriHopProbability, + MinRouteProbability: cfg.MinRouteProbability, + PaymentAttemptPenalty: lnwire.NewMSatFromSatoshis( + btcutil.Amount(cfg.AttemptCost), + ), + PenaltyHalfLife: cfg.PenaltyHalfLife, + } +} diff --git a/lnrpc/routerrpc/config_default.go b/lnrpc/routerrpc/config_default.go index 9ca27faa483e..81c4a577e396 100644 --- a/lnrpc/routerrpc/config_default.go +++ b/lnrpc/routerrpc/config_default.go @@ -2,6 +2,25 @@ package routerrpc -// Config is the default config for the package. When the build tag isn't +import "github.com/lightningnetwork/lnd/routing" + +// Config is the default config struct for the package. When the build tag isn't // specified, then we output a blank config. type Config struct{} + +// DefaultConfig defines the config defaults. Without the sub server enabled, +// there are no defaults to set. +func DefaultConfig() *Config { + return &Config{} +} + +// GetMissionControlConfig returns the mission control config based on this sub +// server config. +func GetMissionControlConfig(cfg *Config) *routing.MissionControlConfig { + return &routing.MissionControlConfig{ + AprioriHopProbability: routing.DefaultAprioriHopProbability, + MinRouteProbability: routing.DefaultMinRouteProbability, + PaymentAttemptPenalty: routing.DefaultPaymentAttemptPenalty, + PenaltyHalfLife: routing.DefaultPenaltyHalfLife, + } +} diff --git a/routing/missioncontrol.go b/routing/missioncontrol.go index 546a64a3e205..7dd47ae06d06 100644 --- a/routing/missioncontrol.go +++ b/routing/missioncontrol.go @@ -14,10 +14,10 @@ import ( ) const ( - // defaultPenaltyHalfLife is the default half-life duration. The + // DefaultPenaltyHalfLife is the default half-life duration. The // half-life duration defines after how much time a penalized node or // channel is back at 50% probability. - defaultPenaltyHalfLife = time.Hour + DefaultPenaltyHalfLife = time.Hour ) // MissionControl contains state which summarizes the past attempts of HTLC @@ -42,9 +42,7 @@ type MissionControl struct { // external function to enable deterministic unit tests. now func() time.Time - // penaltyHalfLife defines after how much time a penalized node or - // channel is back at 50% probability. - penaltyHalfLife time.Duration + cfg *MissionControlConfig sync.Mutex @@ -54,6 +52,28 @@ type MissionControl struct { // TODO(roasbeef): also add favorable metrics for nodes } +// MissionControlConfig defines parameters that control mission control +// behaviour. +type MissionControlConfig struct { + // PenaltyHalfLife defines after how much time a penalized node or + // channel is back at 50% probability. + PenaltyHalfLife time.Duration + + // PaymentAttemptPenalty is the virtual cost in path finding weight + // units of executing a payment attempt that fails. It is used to trade + // off potentially better routes against their probability of + // succeeding. + PaymentAttemptPenalty lnwire.MilliSatoshi + + // MinProbability defines the minimum success probability of the + // returned route. + MinRouteProbability float64 + + // AprioriHopProbability is the assumed success probability of a hop in + // a route when no other information is available. + AprioriHopProbability float64 +} + // nodeHistory contains a summary of payment attempt outcomes involving a // particular node. type nodeHistory struct { @@ -122,15 +142,23 @@ type MissionControlChannelSnapshot struct { // // TODO(roasbeef): persist memory func NewMissionControl(g *channeldb.ChannelGraph, selfNode *channeldb.LightningNode, - qb func(*channeldb.ChannelEdgeInfo) lnwire.MilliSatoshi) *MissionControl { + qb func(*channeldb.ChannelEdgeInfo) lnwire.MilliSatoshi, + cfg *MissionControlConfig) *MissionControl { + + log.Debugf("Instantiating mission control with config: "+ + "PenaltyHalfLife=%v, PaymentAttemptPenalty=%v, "+ + "MinRouteProbability=%v, AprioriHopProbability=%v", + cfg.PenaltyHalfLife, + int64(cfg.PaymentAttemptPenalty.ToSatoshis()), + cfg.MinRouteProbability, cfg.AprioriHopProbability) return &MissionControl{ - history: make(map[route.Vertex]*nodeHistory), - selfNode: selfNode, - queryBandwidth: qb, - graph: g, - now: time.Now, - penaltyHalfLife: defaultPenaltyHalfLife, + history: make(map[route.Vertex]*nodeHistory), + selfNode: selfNode, + queryBandwidth: qb, + graph: g, + now: time.Now, + cfg: cfg, } } @@ -281,7 +309,7 @@ func (m *MissionControl) getEdgeProbability(fromNode route.Vertex, // information becomes available to adjust this probability. nodeHistory, ok := m.history[fromNode] if !ok { - return 1 + return m.cfg.AprioriHopProbability } return m.getEdgeProbabilityForNode(nodeHistory, edge.ChannelID, amt) @@ -310,17 +338,17 @@ func (m *MissionControl) getEdgeProbabilityForNode(nodeHistory *nodeHistory, } if lastFailure == nil { - return 1 + return m.cfg.AprioriHopProbability } timeSinceLastFailure := m.now().Sub(*lastFailure) // Calculate success probability. It is an exponential curve that brings // the probability down to zero when a failure occurs. From there it - // recovers asymptotically back to 1. The rate at which this happens is - // controlled by the penaltyHalfLife parameter. - exp := -timeSinceLastFailure.Hours() / m.penaltyHalfLife.Hours() - probability := 1 - math.Pow(2, exp) + // recovers asymptotically back to the a priori probability. The rate at + // which this happens is controlled by the penaltyHalfLife parameter. + exp := -timeSinceLastFailure.Hours() / m.cfg.PenaltyHalfLife.Hours() + probability := m.cfg.AprioriHopProbability * (1 - math.Pow(2, exp)) return probability } diff --git a/routing/missioncontrol_test.go b/routing/missioncontrol_test.go index ed79c5f20383..19a288286db5 100644 --- a/routing/missioncontrol_test.go +++ b/routing/missioncontrol_test.go @@ -12,9 +12,13 @@ import ( func TestMissionControl(t *testing.T) { now := testTime - mc := NewMissionControl(nil, nil, nil) + mc := NewMissionControl( + nil, nil, nil, &MissionControlConfig{ + PenaltyHalfLife: 30 * time.Minute, + AprioriHopProbability: 0.8, + }, + ) mc.now = func() time.Time { return now } - mc.penaltyHalfLife = 30 * time.Minute testTime := time.Date(2018, time.January, 9, 14, 00, 00, 0, time.UTC) @@ -36,7 +40,7 @@ func TestMissionControl(t *testing.T) { } // Initial probability is expected to be 1. - expectP(1000, 1) + expectP(1000, 0.8) // Expect probability to be zero after reporting the edge as failed. mc.reportEdgeFailure(testEdge, 1000) @@ -44,11 +48,11 @@ func TestMissionControl(t *testing.T) { // As we reported with a min penalization amt, a lower amt than reported // should be unaffected. - expectP(500, 1) + expectP(500, 0.8) // Edge decay started. now = testTime.Add(30 * time.Minute) - expectP(1000, 0.5) + expectP(1000, 0.4) // Edge fails again, this time without a min penalization amt. The edge // should be penalized regardless of amount. @@ -58,7 +62,7 @@ func TestMissionControl(t *testing.T) { // Edge decay started. now = testTime.Add(60 * time.Minute) - expectP(1000, 0.5) + expectP(1000, 0.4) // A node level failure should bring probability of every channel back // to zero. diff --git a/routing/pathfind.go b/routing/pathfind.go index 30ca41f36d70..4e5f2a88b909 100644 --- a/routing/pathfind.go +++ b/routing/pathfind.go @@ -47,9 +47,13 @@ var ( // succeeding. DefaultPaymentAttemptPenalty = lnwire.NewMSatFromSatoshis(100) - // DefaultMinProbability is the default minimum probability for routes + // DefaultMinRouteProbability is the default minimum probability for routes // returned from findPath. - DefaultMinProbability = float64(0.01) + DefaultMinRouteProbability = float64(0.01) + + // DefaultAprioriHopProbability is the default a priori probability for + // a hop. + DefaultAprioriHopProbability = float64(0.95) ) // edgePolicyWithSource is a helper struct to keep track of the source node diff --git a/routing/payment_session.go b/routing/payment_session.go index fca1fb09d120..2c621c0bb1d8 100644 --- a/routing/payment_session.go +++ b/routing/payment_session.go @@ -138,8 +138,8 @@ func (p *paymentSession) RequestRoute(payment *LightningPayment, FeeLimit: payment.FeeLimit, OutgoingChannelID: payment.OutgoingChannelID, CltvLimit: cltvLimit, - PaymentAttemptPenalty: DefaultPaymentAttemptPenalty, - MinProbability: DefaultMinProbability, + PaymentAttemptPenalty: p.mc.cfg.PaymentAttemptPenalty, + MinProbability: p.mc.cfg.MinRouteProbability, }, p.mc.selfNode.PubKeyBytes, payment.Target, payment.Amount, diff --git a/routing/payment_session_test.go b/routing/payment_session_test.go index 3ac01f2a98d8..a8748a624fda 100644 --- a/routing/payment_session_test.go +++ b/routing/payment_session_test.go @@ -35,6 +35,7 @@ func TestRequestRoute(t *testing.T) { session := &paymentSession{ mc: &MissionControl{ selfNode: &channeldb.LightningNode{}, + cfg: &MissionControlConfig{}, }, pathFinder: findPath, } diff --git a/routing/router_test.go b/routing/router_test.go index f478e4b3d418..785a345c3a5a 100644 --- a/routing/router_test.go +++ b/routing/router_test.go @@ -109,6 +109,12 @@ func createTestCtxFromGraphInstance(startingHeight uint32, graphInstance *testGr }, MissionControl: NewMissionControl( graphInstance.graph, selfNode, queryBandwidth, + &MissionControlConfig{ + MinRouteProbability: 0.01, + PaymentAttemptPenalty: 100, + PenaltyHalfLife: time.Hour, + AprioriHopProbability: 0.9, + }, ), }) if err != nil { diff --git a/server.go b/server.go index c35cfdf24927..346ad7b64901 100644 --- a/server.go +++ b/server.go @@ -17,6 +17,8 @@ import ( "sync/atomic" "time" + "github.com/lightningnetwork/lnd/lnrpc/routerrpc" + "github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/connmgr" @@ -632,8 +634,13 @@ func newServer(listenAddrs []net.Addr, chanDB *channeldb.DB, cc *chainControl, return link.Bandwidth() } + // Instantiate mission control with config from the sub server. + // + // TODO(joostjager): When we are further in the process of moving to sub + // servers, the mission control instance itself can be moved there too. s.missionControl = routing.NewMissionControl( chanGraph, selfNode, queryBandwidth, + routerrpc.GetMissionControlConfig(cfg.SubRPCServers.RouterRPC), ) // The router will get access to the payment ID sequencer, such that it