diff --git a/cmd/lncli/cmd_query_mission_control.go b/cmd/lncli/cmd_query_mission_control.go new file mode 100644 index 0000000000..b8764d3784 --- /dev/null +++ b/cmd/lncli/cmd_query_mission_control.go @@ -0,0 +1,59 @@ +// +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 + }{} + + for _, n := range snapshot.Nodes { + displayResp.Nodes = append( + displayResp.Nodes, + 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 f04d456894..73a6ce7572 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 0000000000..4a34d6b1f3 --- /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 0000000000..c2a5fe7bed --- /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 +} diff --git a/config.go b/config.go index a7ba5c6b97..c6e4c875c3 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" @@ -369,7 +370,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 edf9803555..c2ee3a2341 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 9ca27faa48..81c4a577e3 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/lnrpc/routerrpc/router.pb.go b/lnrpc/routerrpc/router.pb.go index f9d0f43368..63c4364469 100644 --- a/lnrpc/routerrpc/router.pb.go +++ b/lnrpc/routerrpc/router.pb.go @@ -737,6 +737,275 @@ func (m *ChannelUpdate) GetExtraOpaqueData() []byte { return nil } +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 + +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,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,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,proto3" json:"channel_id,omitempty"` + /// Time stamp of last failure. + LastFailTime int64 `protobuf:"varint,2,opt,name=last_fail_time,proto3" json:"last_fail_time,omitempty"` + /// Minimum penalization amount. + MinPenalizeAmtSat int64 `protobuf:"varint,3,opt,name=min_penalize_amt_sat,proto3" json:"min_penalize_amt_sat,omitempty"` + /// Estimation of success probability for this channel. + SuccessProb float32 `protobuf:"fixed32,4,opt,name=success_prob,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) GetMinPenalizeAmtSat() int64 { + if m != nil { + return m.MinPenalizeAmtSat + } + 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") @@ -747,89 +1016,109 @@ 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") + 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{ - // 1224 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0x56, 0xef, 0x72, 0x1a, 0x37, - 0x10, 0x2f, 0xb1, 0xcd, 0x9f, 0x05, 0xcc, 0x59, 0xb6, 0x13, 0x4c, 0xe2, 0xc4, 0xa1, 0x9d, 0xd6, - 0x93, 0xe9, 0xd8, 0x53, 0x3a, 0xc9, 0xc7, 0x76, 0x08, 0x88, 0xfa, 0x26, 0x70, 0x47, 0x75, 0xe0, - 0xc4, 0xed, 0x07, 0x8d, 0xcc, 0xc9, 0x70, 0x35, 0xf7, 0xc7, 0x77, 0xa2, 0xb5, 0x5f, 0xa0, 0xaf, - 0xd3, 0xa7, 0xe8, 0x43, 0xf4, 0x11, 0xfa, 0x16, 0x1d, 0x49, 0x77, 0x80, 0x1d, 0xf7, 0x13, 0xa7, - 0xdf, 0xef, 0xa7, 0x5d, 0xed, 0x6a, 0x77, 0x05, 0x3c, 0x8d, 0xc3, 0x85, 0xe0, 0x71, 0x1c, 0x4d, - 0x4e, 0xf5, 0xd7, 0x49, 0x14, 0x87, 0x22, 0x44, 0xa5, 0x25, 0xde, 0x28, 0xc5, 0xd1, 0x44, 0xa3, - 0xcd, 0xbf, 0x73, 0xb0, 0x3d, 0x64, 0x77, 0x3e, 0x0f, 0x04, 0xe1, 0x37, 0x0b, 0x9e, 0x08, 0xf4, - 0x0c, 0x0a, 0x11, 0xbb, 0xa3, 0x31, 0xbf, 0xa9, 0xe7, 0x8e, 0x72, 0xc7, 0x25, 0x92, 0x8f, 0xd8, - 0x1d, 0xe1, 0x37, 0xa8, 0x09, 0xd5, 0x2b, 0xce, 0xe9, 0xdc, 0xf3, 0x3d, 0x41, 0x13, 0x26, 0xea, - 0x4f, 0x8e, 0x72, 0xc7, 0x1b, 0xa4, 0x7c, 0xc5, 0x79, 0x5f, 0x62, 0x0e, 0x13, 0xe8, 0x10, 0x60, - 0x32, 0x17, 0xbf, 0x6b, 0x51, 0x7d, 0xe3, 0x28, 0x77, 0xbc, 0x45, 0x4a, 0x12, 0x51, 0x0a, 0xf4, - 0x0d, 0xd4, 0x84, 0xe7, 0xf3, 0x70, 0x21, 0x68, 0xc2, 0x27, 0x61, 0xe0, 0x26, 0xf5, 0x4d, 0xa5, - 0xd9, 0x4e, 0x61, 0x47, 0xa3, 0xe8, 0x04, 0x76, 0xc3, 0x85, 0x98, 0x86, 0x5e, 0x30, 0xa5, 0x93, - 0x19, 0x0b, 0x02, 0x3e, 0xa7, 0x9e, 0x5b, 0xdf, 0x52, 0x1e, 0x77, 0x32, 0xaa, 0xa3, 0x19, 0xd3, - 0x6d, 0xfe, 0x06, 0xb5, 0x65, 0x18, 0x49, 0x14, 0x06, 0x09, 0x47, 0x07, 0x50, 0x94, 0x71, 0xcc, - 0x58, 0x32, 0x53, 0x81, 0x54, 0x88, 0x8c, 0xeb, 0x8c, 0x25, 0x33, 0xf4, 0x1c, 0x4a, 0x51, 0xcc, - 0xa9, 0xe7, 0xb3, 0x29, 0x57, 0x51, 0x54, 0x48, 0x31, 0x8a, 0xb9, 0x29, 0xd7, 0xe8, 0x15, 0x94, - 0x23, 0x6d, 0x8a, 0xf2, 0x38, 0x56, 0x31, 0x94, 0x08, 0xa4, 0x10, 0x8e, 0xe3, 0xe6, 0x0f, 0x50, - 0x23, 0x32, 0x97, 0x3d, 0xce, 0xb3, 0x9c, 0x21, 0xd8, 0x74, 0x79, 0x22, 0x52, 0x3f, 0xea, 0x5b, - 0xe6, 0x91, 0xf9, 0xeb, 0x89, 0xca, 0x33, 0x5f, 0xe6, 0xa8, 0xe9, 0x82, 0xb1, 0xda, 0x9f, 0x1e, - 0xf6, 0x18, 0x0c, 0x79, 0x3f, 0x32, 0x5c, 0x99, 0x63, 0x5f, 0xee, 0xca, 0xa9, 0x5d, 0xdb, 0x29, - 0xde, 0xe3, 0x7c, 0x90, 0x30, 0x81, 0xbe, 0xd6, 0x29, 0xa4, 0xf3, 0x70, 0x72, 0x4d, 0x5d, 0x3e, - 0x67, 0x77, 0xa9, 0xf9, 0xaa, 0x84, 0xfb, 0xe1, 0xe4, 0xba, 0x2b, 0xc1, 0xe6, 0xaf, 0x80, 0x1c, - 0x1e, 0xb8, 0xa3, 0x50, 0xf9, 0xca, 0x0e, 0xfa, 0x1a, 0x2a, 0x59, 0x70, 0x6b, 0x89, 0xc9, 0x02, - 0x56, 0xc9, 0x69, 0xc2, 0x96, 0x2a, 0x15, 0x65, 0xb6, 0xdc, 0xaa, 0x9c, 0xcc, 0x03, 0x59, 0x2f, - 0xda, 0x8c, 0xa6, 0x9a, 0x14, 0x76, 0xef, 0x19, 0x4f, 0xa3, 0x68, 0x80, 0x4c, 0xa3, 0x4e, 0x6b, - 0x6e, 0x99, 0x56, 0xb5, 0x46, 0xdf, 0x42, 0xe1, 0x8a, 0x79, 0xf3, 0x45, 0x9c, 0x19, 0x46, 0x27, - 0xcb, 0x8a, 0x3c, 0xe9, 0x69, 0x86, 0x64, 0x92, 0xe6, 0x9f, 0x05, 0x28, 0xa4, 0x20, 0x6a, 0xc1, - 0xe6, 0x24, 0x74, 0xb5, 0xc5, 0xed, 0xd6, 0xcb, 0xcf, 0xb7, 0x65, 0xbf, 0x9d, 0xd0, 0xe5, 0x44, - 0x69, 0x51, 0x0b, 0xf6, 0x53, 0x53, 0x34, 0x09, 0x17, 0xf1, 0x84, 0xd3, 0x68, 0x71, 0x79, 0xcd, - 0xef, 0xd2, 0xdb, 0xde, 0x4d, 0x49, 0x47, 0x71, 0x43, 0x45, 0xa1, 0x1f, 0x61, 0x3b, 0x2b, 0xb5, - 0x45, 0xe4, 0x32, 0xc1, 0xd5, 0xdd, 0x97, 0x5b, 0xf5, 0x35, 0x8f, 0x69, 0xc5, 0x8d, 0x15, 0x4f, - 0xaa, 0x93, 0xf5, 0xa5, 0x2c, 0xab, 0x99, 0x98, 0x4f, 0xf4, 0xed, 0xc9, 0xba, 0xde, 0x24, 0x45, - 0x09, 0xa8, 0x7b, 0x6b, 0x42, 0x35, 0x0c, 0xbc, 0x30, 0xa0, 0xc9, 0x8c, 0xd1, 0xd6, 0xdb, 0x77, - 0xaa, 0x96, 0x2b, 0xa4, 0xac, 0x40, 0x67, 0xc6, 0x5a, 0x6f, 0xdf, 0xc9, 0xd2, 0x53, 0xdd, 0xc3, - 0x6f, 0x23, 0x2f, 0xbe, 0xab, 0xe7, 0x8f, 0x72, 0xc7, 0x55, 0xa2, 0x1a, 0x0a, 0x2b, 0x04, 0xed, - 0xc1, 0xd6, 0xd5, 0x9c, 0x4d, 0x93, 0x7a, 0x41, 0x51, 0x7a, 0xd1, 0xfc, 0x67, 0x13, 0xca, 0x6b, - 0x29, 0x40, 0x15, 0x28, 0x12, 0xec, 0x60, 0x72, 0x8e, 0xbb, 0xc6, 0x17, 0xa8, 0x0e, 0x7b, 0x63, - 0xeb, 0x83, 0x65, 0x7f, 0xb4, 0xe8, 0xb0, 0x7d, 0x31, 0xc0, 0xd6, 0x88, 0x9e, 0xb5, 0x9d, 0x33, - 0x23, 0x87, 0x5e, 0x40, 0xdd, 0xb4, 0x3a, 0x36, 0x21, 0xb8, 0x33, 0x5a, 0x72, 0xed, 0x81, 0x3d, - 0xb6, 0x46, 0xc6, 0x13, 0xf4, 0x0a, 0x9e, 0xf7, 0x4c, 0xab, 0xdd, 0xa7, 0x2b, 0x4d, 0xa7, 0x3f, - 0x3a, 0xa7, 0xf8, 0xd3, 0xd0, 0x24, 0x17, 0xc6, 0xc6, 0x63, 0x82, 0xb3, 0x51, 0xbf, 0x93, 0x59, - 0xd8, 0x44, 0x07, 0xb0, 0xaf, 0x05, 0x7a, 0x0b, 0x1d, 0xd9, 0x36, 0x75, 0x6c, 0xdb, 0x32, 0xb6, - 0xd0, 0x0e, 0x54, 0x4d, 0xeb, 0xbc, 0xdd, 0x37, 0xbb, 0x94, 0xe0, 0x76, 0x7f, 0x60, 0xe4, 0xd1, - 0x2e, 0xd4, 0x1e, 0xea, 0x0a, 0xd2, 0x44, 0xa6, 0xb3, 0x2d, 0xd3, 0xb6, 0xe8, 0x39, 0x26, 0x8e, - 0x69, 0x5b, 0x46, 0x11, 0x3d, 0x05, 0x74, 0x9f, 0x3a, 0x1b, 0xb4, 0x3b, 0x46, 0x09, 0xed, 0xc3, - 0xce, 0x7d, 0xfc, 0x03, 0xbe, 0x30, 0x40, 0xa6, 0x41, 0x1f, 0x8c, 0xbe, 0xc7, 0x7d, 0xfb, 0x23, - 0x1d, 0x98, 0x96, 0x39, 0x18, 0x0f, 0x8c, 0x32, 0xda, 0x03, 0xa3, 0x87, 0x31, 0x35, 0x2d, 0x67, - 0xdc, 0xeb, 0x99, 0x1d, 0x13, 0x5b, 0x23, 0xa3, 0xa2, 0x3d, 0x3f, 0x16, 0x78, 0x55, 0x6e, 0xe8, - 0x9c, 0xb5, 0x2d, 0x0b, 0xf7, 0x69, 0xd7, 0x74, 0xda, 0xef, 0xfb, 0xb8, 0x6b, 0x6c, 0xa3, 0x43, - 0x38, 0x18, 0xe1, 0xc1, 0xd0, 0x26, 0x6d, 0x72, 0x41, 0x33, 0xbe, 0xd7, 0x36, 0xfb, 0x63, 0x82, - 0x8d, 0x1a, 0x7a, 0x0d, 0x87, 0x04, 0xff, 0x3c, 0x36, 0x09, 0xee, 0x52, 0xcb, 0xee, 0x62, 0xda, - 0xc3, 0xed, 0xd1, 0x98, 0x60, 0x3a, 0x30, 0x1d, 0xc7, 0xb4, 0x7e, 0x32, 0x0c, 0xf4, 0x15, 0x1c, - 0x2d, 0x25, 0x4b, 0x03, 0x0f, 0x54, 0x3b, 0x32, 0xbe, 0xec, 0x3e, 0x2d, 0xfc, 0x69, 0x44, 0x87, - 0x18, 0x13, 0x03, 0xa1, 0x06, 0x3c, 0x5d, 0xb9, 0xd7, 0x0e, 0x52, 0xdf, 0xbb, 0x92, 0x1b, 0x62, - 0x32, 0x68, 0x5b, 0xf2, 0x82, 0xef, 0x71, 0x7b, 0xf2, 0xd8, 0x2b, 0xee, 0xe1, 0xb1, 0xf7, 0x9b, - 0x7f, 0x6d, 0x40, 0xf5, 0x5e, 0xd1, 0xa3, 0x17, 0x50, 0x4a, 0xbc, 0x69, 0xc0, 0x84, 0x6c, 0x65, - 0xdd, 0xe5, 0x2b, 0x40, 0x3d, 0x00, 0x33, 0xe6, 0x05, 0x7a, 0xbc, 0xe8, 0x6e, 0x2b, 0x29, 0x44, - 0x0d, 0x97, 0x67, 0x50, 0x90, 0x3d, 0x23, 0x67, 0xf9, 0x86, 0x6a, 0x90, 0xbc, 0x5c, 0x9a, 0xae, - 0xb4, 0x2a, 0xe7, 0x57, 0x22, 0x98, 0x1f, 0xa9, 0xde, 0xa9, 0x92, 0x15, 0x80, 0xbe, 0x84, 0xaa, - 0xcf, 0x93, 0x84, 0x4d, 0x39, 0xd5, 0xf5, 0x0f, 0x4a, 0x51, 0x49, 0xc1, 0x9e, 0xc4, 0xa4, 0x28, - 0xeb, 0x5f, 0x2d, 0xda, 0xd2, 0xa2, 0x14, 0xd4, 0xa2, 0x87, 0xe3, 0x53, 0xb0, 0xb4, 0xcd, 0xd6, - 0xc7, 0xa7, 0x60, 0xe8, 0x0d, 0xec, 0xe8, 0x5e, 0xf6, 0x02, 0xcf, 0x5f, 0xf8, 0xba, 0xa7, 0x0b, - 0xea, 0xc8, 0x35, 0xd5, 0xd3, 0x1a, 0x57, 0xad, 0x7d, 0x00, 0xc5, 0x4b, 0x96, 0x70, 0x39, 0xb9, - 0xeb, 0x45, 0x65, 0xac, 0x20, 0xd7, 0x3d, 0xae, 0x1e, 0x21, 0x39, 0xcf, 0x63, 0x39, 0x4d, 0x4a, - 0x9a, 0xba, 0xe2, 0x9c, 0xc8, 0x3c, 0x2e, 0x3d, 0xb0, 0xdb, 0x95, 0x87, 0xf2, 0x9a, 0x07, 0x8d, - 0x2b, 0x0f, 0x6f, 0x60, 0x87, 0xdf, 0x8a, 0x98, 0xd1, 0x30, 0x62, 0x37, 0x0b, 0x4e, 0x5d, 0x26, - 0x58, 0xbd, 0xa2, 0x92, 0x5b, 0x53, 0x84, 0xad, 0xf0, 0x2e, 0x13, 0xac, 0xf5, 0x6f, 0x0e, 0xf2, - 0x6a, 0x2c, 0xc7, 0xa8, 0x0b, 0x65, 0x39, 0xa6, 0xd3, 0x97, 0x11, 0x1d, 0xac, 0x0d, 0xb2, 0xfb, - 0x8f, 0x7e, 0xa3, 0xf1, 0x18, 0x95, 0x4e, 0xf5, 0x0f, 0x60, 0xe0, 0x44, 0x78, 0xbe, 0x9c, 0x78, - 0xe9, 0xbb, 0x85, 0xd6, 0xf5, 0x0f, 0x1e, 0xc3, 0xc6, 0xf3, 0x47, 0xb9, 0xd4, 0x58, 0x5f, 0x1f, - 0x29, 0x7d, 0x39, 0xd0, 0xe1, 0x9a, 0xf6, 0xf3, 0xe7, 0xaa, 0xf1, 0xf2, 0xff, 0x68, 0x6d, 0xed, - 0xfd, 0x77, 0xbf, 0x9c, 0x4e, 0x3d, 0x31, 0x5b, 0x5c, 0x9e, 0x4c, 0x42, 0xff, 0x74, 0xee, 0x4d, - 0x67, 0x22, 0xf0, 0x82, 0x69, 0xc0, 0xc5, 0x1f, 0x61, 0x7c, 0x7d, 0x3a, 0x0f, 0xdc, 0x53, 0xf5, - 0x7a, 0x9d, 0x2e, 0xcd, 0x5c, 0xe6, 0xd5, 0x1f, 0x9f, 0xef, 0xff, 0x0b, 0x00, 0x00, 0xff, 0xff, - 0x89, 0x21, 0x7b, 0xbd, 0x28, 0x09, 0x00, 0x00, + // 1456 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x56, 0xdb, 0x72, 0x1a, 0x47, + 0x13, 0xfe, 0x31, 0x12, 0x87, 0xe6, 0xa0, 0xd5, 0xe8, 0x60, 0x84, 0x2c, 0x5b, 0xde, 0xff, 0xff, + 0x1d, 0x95, 0xcb, 0x25, 0x55, 0x48, 0xd9, 0x95, 0xab, 0xa4, 0x30, 0x2c, 0xd1, 0x96, 0x60, 0xc1, + 0x03, 0xc8, 0x56, 0x72, 0x31, 0x35, 0x62, 0x47, 0xb0, 0x11, 0xec, 0xae, 0x76, 0x87, 0xc4, 0xe4, + 0x01, 0xf2, 0x3a, 0xc9, 0x4d, 0x6e, 0x73, 0x97, 0x87, 0xc8, 0xdb, 0xa4, 0x66, 0x66, 0x97, 0x83, + 0x8c, 0x92, 0x5c, 0xc1, 0x7e, 0xdf, 0x37, 0xdd, 0xd3, 0x3d, 0xdd, 0x3d, 0x03, 0xfb, 0x81, 0x37, + 0xe5, 0x2c, 0x08, 0xfc, 0xc1, 0x99, 0xfa, 0x77, 0xea, 0x07, 0x1e, 0xf7, 0x50, 0x76, 0x8e, 0x97, + 0xb3, 0x81, 0x3f, 0x50, 0xa8, 0xfe, 0x47, 0x02, 0x8a, 0x1d, 0x3a, 0x9b, 0x30, 0x97, 0x63, 0x76, + 0x37, 0x65, 0x21, 0x47, 0x8f, 0x21, 0xed, 0xd3, 0x19, 0x09, 0xd8, 0x5d, 0x29, 0x71, 0x9c, 0x38, + 0xc9, 0xe2, 0x94, 0x4f, 0x67, 0x98, 0xdd, 0x21, 0x1d, 0x0a, 0x37, 0x8c, 0x91, 0xb1, 0x33, 0x71, + 0x38, 0x09, 0x29, 0x2f, 0x3d, 0x3a, 0x4e, 0x9c, 0x24, 0x71, 0xee, 0x86, 0xb1, 0xa6, 0xc0, 0xba, + 0x94, 0xa3, 0x23, 0x80, 0xc1, 0x98, 0xff, 0xa0, 0x44, 0xa5, 0xe4, 0x71, 0xe2, 0x64, 0x13, 0x67, + 0x05, 0x22, 0x15, 0xe8, 0x33, 0xd8, 0xe2, 0xce, 0x84, 0x79, 0x53, 0x4e, 0x42, 0x36, 0xf0, 0x5c, + 0x3b, 0x2c, 0x6d, 0x48, 0x4d, 0x31, 0x82, 0xbb, 0x0a, 0x45, 0xa7, 0xb0, 0xe3, 0x4d, 0xf9, 0xd0, + 0x73, 0xdc, 0x21, 0x19, 0x8c, 0xa8, 0xeb, 0xb2, 0x31, 0x71, 0xec, 0xd2, 0xa6, 0xf4, 0xb8, 0x1d, + 0x53, 0x35, 0xc5, 0x98, 0xb6, 0xfe, 0x3d, 0x6c, 0xcd, 0xc3, 0x08, 0x7d, 0xcf, 0x0d, 0x19, 0x3a, + 0x80, 0x8c, 0x88, 0x63, 0x44, 0xc3, 0x91, 0x0c, 0x24, 0x8f, 0x45, 0x5c, 0xe7, 0x34, 0x1c, 0xa1, + 0x43, 0xc8, 0xfa, 0x01, 0x23, 0xce, 0x84, 0x0e, 0x99, 0x8c, 0x22, 0x8f, 0x33, 0x7e, 0xc0, 0x4c, + 0xf1, 0x8d, 0x9e, 0x41, 0xce, 0x57, 0xa6, 0x08, 0x0b, 0x02, 0x19, 0x43, 0x16, 0x43, 0x04, 0x19, + 0x41, 0xa0, 0x7f, 0x05, 0x5b, 0x58, 0xe4, 0xb2, 0xc1, 0x58, 0x9c, 0x33, 0x04, 0x1b, 0x36, 0x0b, + 0x79, 0xe4, 0x47, 0xfe, 0x17, 0x79, 0xa4, 0x93, 0xe5, 0x44, 0xa5, 0xe8, 0x44, 0xe4, 0x48, 0xb7, + 0x41, 0x5b, 0xac, 0x8f, 0x36, 0x7b, 0x02, 0x9a, 0x38, 0x1f, 0x11, 0xae, 0xc8, 0xf1, 0x44, 0xac, + 0x4a, 0xc8, 0x55, 0xc5, 0x08, 0x6f, 0x30, 0xd6, 0x0a, 0x29, 0x47, 0x2f, 0x54, 0x0a, 0xc9, 0xd8, + 0x1b, 0xdc, 0x12, 0x9b, 0x8d, 0xe9, 0x2c, 0x32, 0x5f, 0x10, 0x70, 0xd3, 0x1b, 0xdc, 0xd6, 0x05, + 0xa8, 0x7f, 0x07, 0xa8, 0xcb, 0x5c, 0xbb, 0xe7, 0x49, 0x5f, 0xf1, 0x46, 0x9f, 0x43, 0x3e, 0x0e, + 0x6e, 0x29, 0x31, 0x71, 0xc0, 0x32, 0x39, 0x3a, 0x6c, 0xca, 0x52, 0x91, 0x66, 0x73, 0x95, 0xfc, + 0xe9, 0xd8, 0x15, 0xf5, 0xa2, 0xcc, 0x28, 0x4a, 0x27, 0xb0, 0xb3, 0x62, 0x3c, 0x8a, 0xa2, 0x0c, + 0x22, 0x8d, 0x2a, 0xad, 0x89, 0x79, 0x5a, 0xe5, 0x37, 0x7a, 0x05, 0xe9, 0x1b, 0xea, 0x8c, 0xa7, + 0x41, 0x6c, 0x18, 0x9d, 0xce, 0x2b, 0xf2, 0xb4, 0xa1, 0x18, 0x1c, 0x4b, 0xf4, 0x9f, 0xd3, 0x90, + 0x8e, 0x40, 0x54, 0x81, 0x8d, 0x81, 0x67, 0x2b, 0x8b, 0xc5, 0xca, 0xd3, 0x4f, 0x97, 0xc5, 0xbf, + 0x35, 0xcf, 0x66, 0x58, 0x6a, 0x51, 0x05, 0xf6, 0x22, 0x53, 0x24, 0xf4, 0xa6, 0xc1, 0x80, 0x11, + 0x7f, 0x7a, 0x7d, 0xcb, 0x66, 0xd1, 0x69, 0xef, 0x44, 0x64, 0x57, 0x72, 0x1d, 0x49, 0xa1, 0xaf, + 0xa1, 0x18, 0x97, 0xda, 0xd4, 0xb7, 0x29, 0x67, 0xf2, 0xec, 0x73, 0x95, 0xd2, 0x92, 0xc7, 0xa8, + 0xe2, 0xfa, 0x92, 0xc7, 0x85, 0xc1, 0xf2, 0xa7, 0x28, 0xab, 0x11, 0x1f, 0x0f, 0xd4, 0xe9, 0x89, + 0xba, 0xde, 0xc0, 0x19, 0x01, 0xc8, 0x73, 0xd3, 0xa1, 0xe0, 0xb9, 0x8e, 0xe7, 0x92, 0x70, 0x44, + 0x49, 0xe5, 0xf5, 0x1b, 0x59, 0xcb, 0x79, 0x9c, 0x93, 0x60, 0x77, 0x44, 0x2b, 0xaf, 0xdf, 0x88, + 0xd2, 0x93, 0xdd, 0xc3, 0x3e, 0xfa, 0x4e, 0x30, 0x2b, 0xa5, 0x8e, 0x13, 0x27, 0x05, 0x2c, 0x1b, + 0xca, 0x90, 0x08, 0xda, 0x85, 0xcd, 0x9b, 0x31, 0x1d, 0x86, 0xa5, 0xb4, 0xa4, 0xd4, 0x87, 0xfe, + 0xe7, 0x06, 0xe4, 0x96, 0x52, 0x80, 0xf2, 0x90, 0xc1, 0x46, 0xd7, 0xc0, 0x97, 0x46, 0x5d, 0xfb, + 0x0f, 0x2a, 0xc1, 0x6e, 0xdf, 0xba, 0xb0, 0xda, 0xef, 0x2d, 0xd2, 0xa9, 0x5e, 0xb5, 0x0c, 0xab, + 0x47, 0xce, 0xab, 0xdd, 0x73, 0x2d, 0x81, 0x9e, 0x40, 0xc9, 0xb4, 0x6a, 0x6d, 0x8c, 0x8d, 0x5a, + 0x6f, 0xce, 0x55, 0x5b, 0xed, 0xbe, 0xd5, 0xd3, 0x1e, 0xa1, 0x67, 0x70, 0xd8, 0x30, 0xad, 0x6a, + 0x93, 0x2c, 0x34, 0xb5, 0x66, 0xef, 0x92, 0x18, 0x1f, 0x3a, 0x26, 0xbe, 0xd2, 0x92, 0xeb, 0x04, + 0xe7, 0xbd, 0x66, 0x2d, 0xb6, 0xb0, 0x81, 0x0e, 0x60, 0x4f, 0x09, 0xd4, 0x12, 0xd2, 0x6b, 0xb7, + 0x49, 0xb7, 0xdd, 0xb6, 0xb4, 0x4d, 0xb4, 0x0d, 0x05, 0xd3, 0xba, 0xac, 0x36, 0xcd, 0x3a, 0xc1, + 0x46, 0xb5, 0xd9, 0xd2, 0x52, 0x68, 0x07, 0xb6, 0xee, 0xeb, 0xd2, 0xc2, 0x44, 0xac, 0x6b, 0x5b, + 0x66, 0xdb, 0x22, 0x97, 0x06, 0xee, 0x9a, 0x6d, 0x4b, 0xcb, 0xa0, 0x7d, 0x40, 0xab, 0xd4, 0x79, + 0xab, 0x5a, 0xd3, 0xb2, 0x68, 0x0f, 0xb6, 0x57, 0xf1, 0x0b, 0xe3, 0x4a, 0x03, 0x91, 0x06, 0xb5, + 0x31, 0xf2, 0xd6, 0x68, 0xb6, 0xdf, 0x93, 0x96, 0x69, 0x99, 0xad, 0x7e, 0x4b, 0xcb, 0xa1, 0x5d, + 0xd0, 0x1a, 0x86, 0x41, 0x4c, 0xab, 0xdb, 0x6f, 0x34, 0xcc, 0x9a, 0x69, 0x58, 0x3d, 0x2d, 0xaf, + 0x3c, 0xaf, 0x0b, 0xbc, 0x20, 0x16, 0xd4, 0xce, 0xab, 0x96, 0x65, 0x34, 0x49, 0xdd, 0xec, 0x56, + 0xdf, 0x36, 0x8d, 0xba, 0x56, 0x44, 0x47, 0x70, 0xd0, 0x33, 0x5a, 0x9d, 0x36, 0xae, 0xe2, 0x2b, + 0x12, 0xf3, 0x8d, 0xaa, 0xd9, 0xec, 0x63, 0x43, 0xdb, 0x42, 0xcf, 0xe1, 0x08, 0x1b, 0xef, 0xfa, + 0x26, 0x36, 0xea, 0xc4, 0x6a, 0xd7, 0x0d, 0xd2, 0x30, 0xaa, 0xbd, 0x3e, 0x36, 0x48, 0xcb, 0xec, + 0x76, 0x4d, 0xeb, 0x1b, 0x4d, 0x43, 0xff, 0x83, 0xe3, 0xb9, 0x64, 0x6e, 0xe0, 0x9e, 0x6a, 0x5b, + 0xc4, 0x17, 0x9f, 0xa7, 0x65, 0x7c, 0xe8, 0x91, 0x8e, 0x61, 0x60, 0x0d, 0xa1, 0x32, 0xec, 0x2f, + 0xdc, 0x2b, 0x07, 0x91, 0xef, 0x1d, 0xc1, 0x75, 0x0c, 0xdc, 0xaa, 0x5a, 0xe2, 0x80, 0x57, 0xb8, + 0x5d, 0xb1, 0xed, 0x05, 0x77, 0x7f, 0xdb, 0x7b, 0xfa, 0x2f, 0x49, 0x28, 0xac, 0x14, 0x3d, 0x7a, + 0x02, 0xd9, 0xd0, 0x19, 0xba, 0x94, 0x8b, 0x56, 0x56, 0x5d, 0xbe, 0x00, 0xe4, 0x05, 0x30, 0xa2, + 0x8e, 0xab, 0xc6, 0x8b, 0xea, 0xb6, 0xac, 0x44, 0xe4, 0x70, 0x79, 0x0c, 0x69, 0xd1, 0x33, 0x62, + 0x96, 0x27, 0x65, 0x83, 0xa4, 0xc4, 0xa7, 0x69, 0x0b, 0xab, 0x62, 0x7e, 0x85, 0x9c, 0x4e, 0x7c, + 0xd9, 0x3b, 0x05, 0xbc, 0x00, 0xd0, 0x7f, 0xa1, 0x30, 0x61, 0x61, 0x48, 0x87, 0x8c, 0xa8, 0xfa, + 0x07, 0xa9, 0xc8, 0x47, 0x60, 0x43, 0x60, 0x42, 0x14, 0xf7, 0xaf, 0x12, 0x6d, 0x2a, 0x51, 0x04, + 0x2a, 0xd1, 0xfd, 0xf1, 0xc9, 0x69, 0xd4, 0x66, 0xcb, 0xe3, 0x93, 0x53, 0xf4, 0x12, 0xb6, 0x55, + 0x2f, 0x3b, 0xae, 0x33, 0x99, 0x4e, 0x54, 0x4f, 0xa7, 0xe5, 0x96, 0xb7, 0x64, 0x4f, 0x2b, 0x5c, + 0xb6, 0xf6, 0x01, 0x64, 0xae, 0x69, 0xc8, 0xc4, 0xe4, 0x2e, 0x65, 0xa4, 0xb1, 0xb4, 0xf8, 0x6e, + 0x30, 0x79, 0x09, 0x89, 0x79, 0x1e, 0x88, 0x69, 0x92, 0x55, 0xd4, 0x0d, 0x63, 0x58, 0xe4, 0x71, + 0xee, 0x81, 0x7e, 0x5c, 0x78, 0xc8, 0x2d, 0x79, 0x50, 0xb8, 0xf4, 0xf0, 0x12, 0xb6, 0xd9, 0x47, + 0x1e, 0x50, 0xe2, 0xf9, 0xf4, 0x6e, 0xca, 0x88, 0x4d, 0x39, 0x2d, 0xe5, 0x65, 0x72, 0xb7, 0x24, + 0xd1, 0x96, 0x78, 0x9d, 0x72, 0xaa, 0x3f, 0x81, 0x32, 0x66, 0x21, 0xe3, 0x2d, 0x27, 0x0c, 0x1d, + 0xcf, 0xad, 0x79, 0x2e, 0x0f, 0xbc, 0x71, 0x74, 0x01, 0xe8, 0x47, 0x70, 0xb8, 0x96, 0x55, 0x13, + 0x5c, 0x2c, 0x7e, 0x37, 0x65, 0xc1, 0x6c, 0xfd, 0xe2, 0x0b, 0x38, 0x5c, 0xcb, 0x46, 0xe3, 0xff, + 0x15, 0x6c, 0xba, 0x9e, 0xcd, 0xc2, 0x52, 0xe2, 0x38, 0x79, 0x92, 0xab, 0xec, 0x2f, 0xcd, 0x4d, + 0xcb, 0xb3, 0xd9, 0xb9, 0x13, 0x72, 0x2f, 0x98, 0x61, 0x25, 0xd2, 0x7f, 0x4f, 0x40, 0x6e, 0x09, + 0x46, 0xfb, 0x90, 0x8a, 0x66, 0xb4, 0x2a, 0xaa, 0xe8, 0x0b, 0xbd, 0x80, 0xe2, 0x98, 0x86, 0x9c, + 0x88, 0x91, 0x4d, 0xc4, 0x21, 0x45, 0xf7, 0xdd, 0x3d, 0x14, 0x7d, 0x09, 0x8f, 0x3d, 0x3e, 0x62, + 0x81, 0x7c, 0x2f, 0x90, 0x70, 0x3a, 0x18, 0xb0, 0x30, 0x24, 0x7e, 0xe0, 0x5d, 0xcb, 0x52, 0x7b, + 0x84, 0x1f, 0xa2, 0xd1, 0x6b, 0xc8, 0x44, 0x35, 0x22, 0x9e, 0x23, 0x62, 0xeb, 0x07, 0x9f, 0x8e, + 0xfc, 0x78, 0xf7, 0x73, 0xa9, 0xfe, 0x6b, 0x02, 0x8a, 0xab, 0x24, 0x7a, 0x2a, 0xab, 0x3f, 0x7e, + 0xad, 0x24, 0xe4, 0x61, 0x2e, 0x21, 0xff, 0x3a, 0x96, 0x0a, 0xec, 0x4e, 0x1c, 0x97, 0xf8, 0xcc, + 0xa5, 0x63, 0xe7, 0x27, 0x46, 0xe2, 0x87, 0x44, 0x52, 0xaa, 0xd7, 0x72, 0x48, 0x87, 0xfc, 0x4a, + 0xd0, 0x1b, 0x32, 0xe8, 0x15, 0xac, 0xf2, 0x5b, 0x12, 0x52, 0xf2, 0xca, 0x0e, 0x50, 0x1d, 0x72, + 0xe2, 0x0a, 0x8f, 0x5e, 0x4d, 0x68, 0x39, 0xe2, 0xd5, 0x07, 0x61, 0xb9, 0xbc, 0x8e, 0x8a, 0x8e, + 0xfc, 0x02, 0x34, 0x23, 0xe4, 0xce, 0x44, 0xdc, 0x86, 0xd1, 0x9b, 0x06, 0x2d, 0xeb, 0xef, 0x3d, + 0x94, 0xca, 0x87, 0x6b, 0xb9, 0xc8, 0x58, 0x53, 0x6d, 0x29, 0x7a, 0x55, 0xa0, 0xa3, 0x25, 0xed, + 0xa7, 0x4f, 0x99, 0xf2, 0xd3, 0x87, 0xe8, 0xc8, 0x9a, 0x0d, 0x3b, 0x6b, 0x2a, 0x1d, 0xfd, 0x7f, + 0x79, 0x07, 0x0f, 0xf6, 0x49, 0xf9, 0xc5, 0x3f, 0xc9, 0x16, 0x5e, 0xd6, 0xb4, 0xc4, 0x8a, 0x97, + 0x87, 0x1b, 0x6a, 0xc5, 0xcb, 0xdf, 0x74, 0xd6, 0xdb, 0xcf, 0xbf, 0x3d, 0x1b, 0x3a, 0x7c, 0x34, + 0xbd, 0x3e, 0x1d, 0x78, 0x93, 0xb3, 0xb1, 0x33, 0x1c, 0x71, 0xd7, 0x71, 0x87, 0x2e, 0xe3, 0x3f, + 0x7a, 0xc1, 0xed, 0xd9, 0xd8, 0xb5, 0xcf, 0xe4, 0x2b, 0xed, 0x6c, 0x6e, 0xee, 0x3a, 0x25, 0x1f, + 0xf8, 0x5f, 0xfc, 0x15, 0x00, 0x00, 0xff, 0xff, 0xe1, 0x3e, 0x2a, 0xde, 0x10, 0x0c, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -860,6 +1149,14 @@ 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) + //* + //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 { @@ -897,6 +1194,24 @@ 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 +} + +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 { //* @@ -915,6 +1230,14 @@ 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) + //* + //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) { @@ -975,6 +1298,42 @@ 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) +} + +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), @@ -991,6 +1350,14 @@ var _Router_serviceDesc = grpc.ServiceDesc{ MethodName: "SendToRoute", Handler: _Router_SendToRoute_Handler, }, + { + 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 dc875d680b..f65a8c11eb 100644 --- a/lnrpc/routerrpc/router.proto +++ b/lnrpc/routerrpc/router.proto @@ -245,6 +245,46 @@ message ChannelUpdate { */ bytes extra_opaque_data = 12; } +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 [json_name = "pubkey"]; + + /// Time stamp of last failure. Set to zero if no failure happened yet. + int64 last_fail_time = 2 [json_name = "last_fail_time"]; + + /// Estimation of success probability for channels not in the channel array. + float other_chan_success_prob = 3 [json_name = "other_chan_success_prob"]; + + /// Historical information of particular channels. + repeated ChannelHistory channels = 4 [json_name = "channels"]; +} + +/// NodeHistory contains the mission control state for a particular channel. +message ChannelHistory { + /// Short channel id + uint64 channel_id = 1 [json_name = "channel_id"]; + + /// Time stamp of last failure. + int64 last_fail_time = 2 [json_name = "last_fail_time"]; + + /// Minimum penalization amount. + int64 min_penalize_amt_sat = 3 [json_name = "min_penalize_amt_sat"]; + + /// Estimation of success probability for this channel. + float success_prob = 4 [json_name = "success_prob"]; +} service Router { /** @@ -268,4 +308,16 @@ 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); + + /** + 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_backend.go b/lnrpc/routerrpc/router_backend.go index 2b66f1f7ca..9e399e8fc7 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 @@ -121,9 +123,22 @@ 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, + amt lnwire.MilliSatoshi) 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 bfe7a488c8..49af5387c7 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, + ) != 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, + ) != 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{}, 0, + ) != 1 { + t.Fatal("expecting 100% probability") } hops := []*route.Hop{{}} diff --git a/lnrpc/routerrpc/router_server.go b/lnrpc/routerrpc/router_server.go index 9f08237adf..9f1ca20854 100644 --- a/lnrpc/routerrpc/router_server.go +++ b/lnrpc/routerrpc/router_server.go @@ -59,6 +59,14 @@ var ( Entity: "offchain", Action: "read", }}, + "/routerrpc.Router/QueryMissionControl": {{ + Entity: "offchain", + Action: "read", + }}, + "/routerrpc.Router/ResetMissionControl": {{ + Entity: "offchain", + Action: "write", + }}, } // DefaultRouterMacFilename is the default name of the router macaroon @@ -439,3 +447,56 @@ func marshallChannelUpdate(update *lnwire.ChannelUpdate) *ChannelUpdate { ExtraOpaqueData: update.ExtraOpaqueData, } } + +// 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 +} + +// 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(), + MinPenalizeAmtSat: int64( + channel.MinPenalizeAmt.ToSatoshis(), + ), + SuccessProb: float32(channel.SuccessProb), + } + } + + var lastFail int64 + if node.LastFail != nil { + lastFail = node.LastFail.Unix() + } + + rpcNodes[i] = &NodeHistory{ + Pubkey: node.Node[:], + LastFailTime: lastFail, + OtherChanSuccessProb: float32( + node.OtherChanSuccessProb, + ), + Channels: channels, + } + } + + response := QueryMissionControlResponse{ + Nodes: rpcNodes, + } + + return &response, nil +} diff --git a/lntest/itest/lnd_test.go b/lntest/itest/lnd_test.go index 9435853697..6381c24f05 100644 --- a/lntest/itest/lnd_test.go +++ b/lntest/itest/lnd_test.go @@ -32,6 +32,7 @@ import ( "github.com/lightningnetwork/lnd" "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" @@ -8382,8 +8383,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/lntest/node.go b/lntest/node.go index 622a0e3247..fd63afb5b9 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" ) @@ -248,6 +249,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. @@ -497,6 +502,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/heap.go b/routing/heap.go index 80336fd0ce..be7acaf0ad 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/missioncontrol.go b/routing/missioncontrol.go index 9cbaa9115d..17575a7218 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,12 @@ 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 + + cfg *MissionControlConfig + sync.Mutex // TODO(roasbeef): further counters, if vertex continually unavailable, @@ -74,83 +56,113 @@ type MissionControl struct { // PaymentSessionSource interface. var _ PaymentSessionSource = (*MissionControl)(nil) -// 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 { +// 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 +} - return &MissionControl{ - failedEdges: make(map[EdgeLocator]time.Time), - failedVertexes: make(map[route.Vertex]time.Time), - selfNode: selfNode, - queryBandwidth: qb, - graph: g, - } +// 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 + + // 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() +// 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 +} - m.Lock() +// MissionControlNodeSnapshot contains a snapshot of the current node state in +// mission control. +type MissionControlNodeSnapshot struct { + // Node pubkey. + Node route.Vertex - // 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 - } + // Lastfail is the time of last failure, if any. + LastFail *time.Time - vertexes[vertex] = struct{}{} - } + // Channels is a list of channels for which specific information is + // logged. + Channels []MissionControlChannelSnapshot - // 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) + // OtherChanSuccessProb is the success probability for channels not in + // the Channels slice. + OtherChanSuccessProb float64 +} - delete(m.failedEdges, edge) - continue - } +// 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 - edges[edge] = struct{}{} - } + // LastFail is the time of last failure. + LastFail time.Time - m.Unlock() + // MinPenalizeAmt is the minimum amount for which the channel will be + // penalized. + MinPenalizeAmt lnwire.MilliSatoshi - log.Debugf("Mission Control returning prune view of %v edges, %v "+ - "vertexes", len(edges), len(vertexes)) + // SuccessProb is the success probability estimation for this channel. + SuccessProb float64 +} + +// 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, + cfg *MissionControlConfig) *MissionControl { - return graphPruneView{ - edges: edges, - vertexes: vertexes, + 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, + cfg: cfg, } } @@ -161,8 +173,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 @@ -226,10 +236,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 @@ -239,8 +248,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, } @@ -251,8 +259,7 @@ func (m *MissionControl) NewPaymentSessionForRoute(preBuiltRoute *route.Route) P // missioncontrol for resumed payment we don't want to make more attempts for. func (m *MissionControl) NewPaymentSessionEmpty() PaymentSession { return &paymentSession{ - pruneViewSnapshot: m.graphPruneView(), - errFailedPolicyChans: make(map[EdgeLocator]struct{}), + errFailedPolicyChans: make(map[nodeChannel]struct{}), mc: m, preBuiltRoute: &route.Route{}, preBuiltRouteTried: true, @@ -298,7 +305,174 @@ 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 a constant a priori + // probability. After the attempt new information becomes available to + // adjust this probability. + nodeHistory, ok := m.history[fromNode] + if !ok { + return m.cfg.AprioriHopProbability + } + + 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. We only take into account a previous failure if the + // amount that we currently get the probability for is greater or equal + // than the minPenalizeAmt of the previous failure. + channelHistory, ok := nodeHistory.channelLastFail[channelID] + if ok && channelHistory.minPenalizeAmt <= amt { + + // If there is both a node level failure recorded and a channel + // level failure is applicable too, we take the most recent of + // the two. + if lastFailure == nil || + channelHistory.lastFail.After(*lastFailure) { + + lastFailure = &channelHistory.lastFail + } + } + + if lastFailure == nil { + 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 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 +} + +// 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, + } +} + +// 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 new file mode 100644 index 0000000000..19a288286d --- /dev/null +++ b/routing/missioncontrol_test.go @@ -0,0 +1,81 @@ +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, &MissionControlConfig{ + PenaltyHalfLife: 30 * time.Minute, + AprioriHopProbability: 0.8, + }, + ) + mc.now = func() time.Time { return now } + + 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, 0.8) + + // 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, 0.8) + + // Edge decay started. + now = testTime.Add(30 * time.Minute) + expectP(1000, 0.4) + + // 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.4) + + // A node level failure should bring probability of every channel back + // 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") + } +} diff --git a/routing/mock_test.go b/routing/mock_test.go index 6d98dd6efa..c5314b8305 100644 --- a/routing/mock_test.go +++ b/routing/mock_test.go @@ -112,10 +112,9 @@ func (m *mockPaymentSession) RequestRoute(payment *LightningPayment, func (m *mockPaymentSession) ReportVertexFailure(v route.Vertex) {} -func (m *mockPaymentSession) ReportEdgeFailure(e *EdgeLocator) {} +func (m *mockPaymentSession) ReportEdgeFailure(failedEdge edge, minPenalizeAmt lnwire.MilliSatoshi) {} -func (m *mockPaymentSession) ReportEdgePolicyFailure(errSource route.Vertex, failedEdge *EdgeLocator) { -} +func (m *mockPaymentSession) ReportEdgePolicyFailure(failedEdge edge) {} type mockPayer struct { sendResult chan error diff --git a/routing/pathfind.go b/routing/pathfind.go index 2224798547..1e3676bc3f 100644 --- a/routing/pathfind.go +++ b/routing/pathfind.go @@ -40,6 +40,22 @@ 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) + + // DefaultMinRouteProbability is the default minimum probability for routes + // returned from findPath. + 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 // of a channel edge. ChannelEdgePolicy only contains to destination node // of the edge. @@ -228,13 +244,10 @@ 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, + lnwire.MilliSatoshi) float64 // FeeLimit is a maximum fee amount allowed to be used on the path from // the source to the target. @@ -248,6 +261,16 @@ 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 + + // MinProbability defines the minimum success probability of the + // returned route. + MinProbability float64 } // findPath attempts to find a path from the source node within the @@ -331,10 +354,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 +366,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,20 +395,25 @@ 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 { - return - } + edgeProbability := r.ProbabilitySource( + fromVertex, *locator, amountToSend, + ) - toNodeDist := distance[toNode] + log.Tracef("path finding probability: fromnode=%v, chanid=%v, "+ + "probability=%v", fromVertex, locator.ChannelID, + edgeProbability) - amountToSend := toNodeDist.amountToReceive + // If the probability is zero, there is no point in trying. + if edgeProbability == 0 { + return + } // If the estimated bandwidth of the channel edge is not able // to carry the amount that needs to be send, return. @@ -453,19 +473,39 @@ 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 + + // 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 // 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 - - // If this new tentative distance is not better than the current - // best known distance to this node, return. + 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 tempDist >= distance[fromVertex].dist { return } @@ -483,10 +523,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 +655,53 @@ func findPath(g *graphParams, r *RestrictParams, source, target route.Vertex, "too many hops") } + log.Debugf("Found route: probability=%v, hops=%v, fee=%v\n", + distance[source].probability, numEdges, + distance[source].amountToReceive-amt) + 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 7ce114c290..8a5cf06877 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, lnwire.MilliSatoshi) float64 { + return 1 +} + // testGraph is the struct which corresponds to the JSON format used to encode // graphs within the files in the testdata directory. // @@ -477,6 +484,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 +502,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 +527,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 +553,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 @@ -625,9 +642,7 @@ func TestFindLowestFeePath(t *testing.T) { &graphParams{ graph: testGraphInstance.graph, }, - &RestrictParams{ - FeeLimit: noFeeLimit, - }, + noRestrictions, sourceNode.PubKeyBytes, target, paymentAmt, ) if err != nil { @@ -766,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, ) @@ -934,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 { @@ -1190,9 +1204,7 @@ func TestNewRoutePathTooLong(t *testing.T) { &graphParams{ graph: graph.graph, }, - &RestrictParams{ - FeeLimit: noFeeLimit, - }, + noRestrictions, sourceNode.PubKeyBytes, target, paymentAmt, ) if err != nil { @@ -1206,9 +1218,7 @@ func TestNewRoutePathTooLong(t *testing.T) { &graphParams{ graph: graph.graph, }, - &RestrictParams{ - FeeLimit: noFeeLimit, - }, + noRestrictions, sourceNode.PubKeyBytes, target, paymentAmt, ) if err == nil { @@ -1248,9 +1258,7 @@ func TestPathNotAvailable(t *testing.T) { &graphParams{ graph: graph.graph, }, - &RestrictParams{ - FeeLimit: noFeeLimit, - }, + noRestrictions, sourceNode.PubKeyBytes, unknownNode, 100, ) if !IsError(err, ErrNoPathFound) { @@ -1287,9 +1295,7 @@ func TestPathInsufficientCapacity(t *testing.T) { &graphParams{ graph: graph.graph, }, - &RestrictParams{ - FeeLimit: noFeeLimit, - }, + noRestrictions, sourceNode.PubKeyBytes, target, payAmt, ) if !IsError(err, ErrNoPathFound) { @@ -1322,9 +1328,7 @@ func TestRouteFailMinHTLC(t *testing.T) { &graphParams{ graph: graph.graph, }, - &RestrictParams{ - FeeLimit: noFeeLimit, - }, + noRestrictions, sourceNode.PubKeyBytes, target, payAmt, ) if !IsError(err, ErrNoPathFound) { @@ -1382,9 +1386,7 @@ func TestRouteFailMaxHTLC(t *testing.T) { &graphParams{ graph: graph.graph, }, - &RestrictParams{ - FeeLimit: noFeeLimit, - }, + noRestrictions, sourceNode.PubKeyBytes, target, payAmt, ) if err != nil { @@ -1406,9 +1408,7 @@ func TestRouteFailMaxHTLC(t *testing.T) { &graphParams{ graph: graph.graph, }, - &RestrictParams{ - FeeLimit: noFeeLimit, - }, + noRestrictions, sourceNode.PubKeyBytes, target, payAmt, ) if !IsError(err, ErrNoPathFound) { @@ -1443,9 +1443,7 @@ func TestRouteFailDisabledEdge(t *testing.T) { &graphParams{ graph: graph.graph, }, - &RestrictParams{ - FeeLimit: noFeeLimit, - }, + noRestrictions, sourceNode.PubKeyBytes, target, payAmt, ) if err != nil { @@ -1473,9 +1471,7 @@ func TestRouteFailDisabledEdge(t *testing.T) { &graphParams{ graph: graph.graph, }, - &RestrictParams{ - FeeLimit: noFeeLimit, - }, + noRestrictions, sourceNode.PubKeyBytes, target, payAmt, ) if err != nil { @@ -1500,9 +1496,7 @@ func TestRouteFailDisabledEdge(t *testing.T) { &graphParams{ graph: graph.graph, }, - &RestrictParams{ - FeeLimit: noFeeLimit, - }, + noRestrictions, sourceNode.PubKeyBytes, target, payAmt, ) if !IsError(err, ErrNoPathFound) { @@ -1536,9 +1530,7 @@ func TestPathSourceEdgesBandwidth(t *testing.T) { &graphParams{ graph: graph.graph, }, - &RestrictParams{ - FeeLimit: noFeeLimit, - }, + noRestrictions, sourceNode.PubKeyBytes, target, payAmt, ) if err != nil { @@ -1562,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) { @@ -1582,9 +1572,7 @@ func TestPathSourceEdgesBandwidth(t *testing.T) { graph: graph.graph, bandwidthHints: bandwidths, }, - &RestrictParams{ - FeeLimit: noFeeLimit, - }, + noRestrictions, sourceNode.PubKeyBytes, target, payAmt, ) if err != nil { @@ -1615,9 +1603,7 @@ func TestPathSourceEdgesBandwidth(t *testing.T) { graph: graph.graph, bandwidthHints: bandwidths, }, - &RestrictParams{ - FeeLimit: noFeeLimit, - }, + noRestrictions, sourceNode.PubKeyBytes, target, payAmt, ) if err != nil { @@ -1906,6 +1892,7 @@ func TestRestrictOutgoingChannel(t *testing.T) { &RestrictParams{ FeeLimit: noFeeLimit, OutgoingChannelID: &outgoingChannelID, + ProbabilitySource: noProbabilitySource, }, sourceVertex, target, paymentAmt, ) @@ -1982,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"] @@ -1999,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, ) @@ -2035,3 +2018,170 @@ 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 + minProbability 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, + minProbability: 0.1, + expectedChan: 10, + }, + { + name: "three hop 2", + p10: 0.5, p11: 0.8, p20: 0.7, + minProbability: 0.1, + 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, + 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.minProbability, tc.expectedChan, + ) + }) + } +} + +func testProbabilityRouting(t *testing.T, p10, p11, p20, minProbability 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("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, + amt lnwire.MilliSatoshi) float64 { + + if amt == 0 { + t.Fatal("expected non-zero amount") + } + + 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), + 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) + } + + // 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 28047f537f..7508e30a34 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" @@ -24,11 +23,12 @@ type PaymentSession interface { // route. ReportVertexFailure(v route.Vertex) - // ReportEdgeFailure reports to the PaymentSession that the passed - // channel failed to route the previous payment attempt. The - // PaymentSession will use this information to produce a better next - // route. - ReportEdgeFailure(e *EdgeLocator) + // ReportEdgeFailure reports to the PaymentSession that the passed edge + // failed to route the previous payment attempt. A minimum penalization + // amount is included to attenuate the failure. This is set to a + // non-zero value for channel balance failures. The PaymentSession will + // use this information to produce a better next route. + ReportEdgeFailure(failedEdge edge, minPenalizeAmt lnwire.MilliSatoshi) // ReportEdgePolicyFailure reports to the PaymentSession that we // received a failure message that relates to a channel policy. For @@ -36,7 +36,7 @@ type PaymentSession interface { // keep the edge included in the next attempted route. The // PaymentSession will use this information to produce a better next // route. - ReportEdgePolicyFailure(errSource route.Vertex, failedEdge *EdgeLocator) + ReportEdgePolicyFailure(failedEdge edge) } // paymentSession is used during an HTLC routings session to prune the local @@ -48,8 +48,6 @@ type PaymentSession interface { // 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 @@ -58,7 +56,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 @@ -80,17 +78,7 @@ var _ PaymentSession = (*paymentSession)(nil) // // NOTE: Part of the PaymentSession interface. 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) } // ReportEdgeFailure adds a channel to the graph prune view. The time the @@ -102,18 +90,10 @@ func (p *paymentSession) ReportVertexFailure(v route.Vertex) { // TODO(roasbeef): also add value attempted to send and capacity of channel // // NOTE: Part of the PaymentSession interface. -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) } // ReportEdgePolicyFailure handles a failure message that relates to a @@ -124,23 +104,28 @@ func (p *paymentSession) ReportEdgeFailure(e *EdgeLocator) { // new channel updates. // // NOTE: Part of the PaymentSession interface. -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{}{} + p.errFailedPolicyChans[key] = struct{}{} } // RequestRoute returns a route which is likely to be capable for successfully @@ -169,15 +154,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 @@ -200,11 +176,12 @@ 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.mc.getEdgeProbability, + FeeLimit: payment.FeeLimit, + OutgoingChannelID: payment.OutgoingChannelID, + CltvLimit: cltvLimit, + PaymentAttemptPenalty: p.mc.cfg.PaymentAttemptPenalty, + MinProbability: p.mc.cfg.MinRouteProbability, }, p.mc.selfNode.PubKeyBytes, payment.Target, payment.Amount, @@ -227,3 +204,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 f730bc5767..a8748a624f 100644 --- a/routing/payment_session_test.go +++ b/routing/payment_session_test.go @@ -35,9 +35,9 @@ func TestRequestRoute(t *testing.T) { session := &paymentSession{ mc: &MissionControl{ selfNode: &channeldb.LightningNode{}, + cfg: &MissionControlConfig{}, }, - pruneViewSnapshot: graphPruneView{}, - pathFinder: findPath, + pathFinder: findPath, } cltvLimit := uint32(30) diff --git a/routing/route/route.go b/routing/route/route.go index 2cd046849f..ad639b5399 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" @@ -180,3 +182,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(), + ) +} diff --git a/routing/router.go b/routing/router.go index 37443b2df8..a78791ea26 100644 --- a/routing/router.go +++ b/routing/router.go @@ -319,6 +319,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 @@ -1598,7 +1605,9 @@ 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. @@ -1767,7 +1776,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 } @@ -1799,13 +1810,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) { @@ -1896,7 +1905,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 @@ -1904,7 +1913,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 @@ -1929,7 +1938,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 @@ -1960,14 +1969,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: @@ -1977,12 +1984,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 @@ -2001,17 +2010,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 diff --git a/routing/router_test.go b/routing/router_test.go index f90dbd8c52..4c2b2be976 100644 --- a/routing/router_test.go +++ b/routing/router_test.go @@ -95,6 +95,12 @@ func createTestCtxFromGraphInstance(startingHeight uint32, graphInstance *testGr func(e *channeldb.ChannelEdgeInfo) lnwire.MilliSatoshi { return lnwire.NewMSatFromSatoshis(e.Capacity) }, + &MissionControlConfig{ + MinRouteProbability: 0.01, + PaymentAttemptPenalty: 100, + PenaltyHalfLife: time.Hour, + AprioriHopProbability: 0.9, + }, ) router, err := New(Config{ Graph: graphInstance.graph, @@ -201,7 +207,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( @@ -2198,9 +2205,7 @@ func TestFindPathFeeWeighting(t *testing.T) { &graphParams{ graph: ctx.graph, }, - &RestrictParams{ - FeeLimit: noFeeLimit, - }, + noRestrictions, sourceNode.PubKeyBytes, target, amt, ) if err != nil { diff --git a/rpcserver.go b/rpcserver.go index eebed0d260..e2381987b6 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -476,7 +476,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 8c5fafe064..64293b3e02 100644 --- a/server.go +++ b/server.go @@ -38,6 +38,7 @@ import ( "github.com/lightningnetwork/lnd/lncfg" "github.com/lightningnetwork/lnd/lnpeer" "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/routerrpc" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/nat" @@ -187,6 +188,8 @@ type server struct { breachArbiter *breachArbiter + missionControl *routing.MissionControl + chanRouter *routing.ChannelRouter authGossiper *discovery.AuthenticatedGossiper @@ -617,15 +620,6 @@ func newServer(listenAddrs []net.Addr, chanDB *channeldb.DB, cc *chainControl, } 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 { @@ -646,8 +640,13 @@ func newServer(listenAddrs []net.Addr, chanDB *channeldb.DB, cc *chainControl, return link.Bandwidth() } - missionControl := routing.NewMissionControl( + // 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), ) s.chanRouter, err = routing.New(routing.Config{ @@ -656,7 +655,7 @@ func newServer(listenAddrs []net.Addr, chanDB *channeldb.DB, cc *chainControl, ChainView: cc.chainView, Payer: s.htlcSwitch, Control: channeldb.NewPaymentControl(chanDB), - MissionControl: missionControl, + MissionControl: s.missionControl, ChannelPruneExpiry: routing.DefaultChannelPruneExpiry, GraphPruneInterval: time.Duration(time.Hour), QueryBandwidth: queryBandwidth,