diff --git a/cmd/lncli/cmd_mission_control.go b/cmd/lncli/cmd_mission_control.go index 9d8b5533b..76c76f992 100644 --- a/cmd/lncli/cmd_mission_control.go +++ b/cmd/lncli/cmd_mission_control.go @@ -7,6 +7,7 @@ import ( "github.com/btcsuite/btcd/btcutil" "github.com/lightningnetwork/lnd/lnrpc/routerrpc" "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing" "github.com/lightningnetwork/lnd/routing/route" "github.com/urfave/cli" ) @@ -45,26 +46,12 @@ var setCfgCommand = cli.Command{ Category: "Mission Control", Usage: "Set mission control's config.", Description: ` - Update the config values being used by mission control to calculate - the probability that payment routes will succeed. - `, + Update the config values being used by mission control to calculate the + probability that payment routes will succeed. The estimator type must be + provided to set estimator-related parameters.`, Flags: []cli.Flag{ - cli.DurationFlag{ - Name: "halflife", - Usage: "the amount of time taken to restore a node " + - "or channel to 50% probability of success.", - }, - cli.Float64Flag{ - Name: "hopprob", - Usage: "the probability of success assigned " + - "to hops that we have no information about", - }, - cli.Float64Flag{ - Name: "weight", - Usage: "the degree to which mission control should " + - "rely on historical results, expressed as " + - "value in [0;1]", - }, cli.UintFlag{ + // General settings. + cli.UintFlag{ Name: "pmtnr", Usage: "the number of payments mission control " + "should store", @@ -74,6 +61,48 @@ var setCfgCommand = cli.Command{ Usage: "the amount of time to wait after a failure " + "before raising failure amount", }, + // Probability estimator. + cli.StringFlag{ + Name: "estimator", + Usage: "the probability estimator to use, choose " + + "between 'apriori' or 'bimodal'", + }, + // Apriori config. + cli.DurationFlag{ + Name: "apriorihalflife", + Usage: "the amount of time taken to restore a node " + + "or channel to 50% probability of success.", + }, + cli.Float64Flag{ + Name: "apriorihopprob", + Usage: "the probability of success assigned " + + "to hops that we have no information about", + }, + cli.Float64Flag{ + Name: "aprioriweight", + Usage: "the degree to which mission control should " + + "rely on historical results, expressed as " + + "value in [0, 1]", + }, + // Bimodal config. + cli.DurationFlag{ + Name: "bimodaldecaytime", + Usage: "the time span after which we phase out " + + "learnings from previous payment attempts", + }, + cli.Uint64Flag{ + Name: "bimodalscale", + Usage: "controls the assumed channel liquidity " + + "imbalance in the network, measured in msat. " + + "a low value (compared to typical channel " + + "capacity) anticipates unbalanced channels.", + }, + cli.Float64Flag{ + Name: "bimodalweight", + Usage: "controls the degree to which the probability " + + "estimator takes into account other channels " + + "of a router", + }, }, Action: actionDecorator(setCfg), } @@ -85,51 +114,140 @@ func setCfg(ctx *cli.Context) error { client := routerrpc.NewRouterClient(conn) - resp, err := client.GetMissionControlConfig( + // Fetch current mission control config which we update to create our + // response. + mcCfg, err := client.GetMissionControlConfig( ctxc, &routerrpc.GetMissionControlConfigRequest{}, ) if err != nil { return err } + // haveValue is a helper variable to determine if a flag has been set or + // the help should be displayed. var haveValue bool - if ctx.IsSet("halflife") { - haveValue = true - resp.Config.HalfLifeSeconds = uint64(ctx.Duration( - "halflife", - ).Seconds()) - } - - if ctx.IsSet("hopprob") { - haveValue = true - resp.Config.HopProbability = float32(ctx.Float64("hopprob")) - } - - if ctx.IsSet("weight") { - haveValue = true - resp.Config.Weight = float32(ctx.Float64("weight")) - } - + // Handle general mission control settings. if ctx.IsSet("pmtnr") { haveValue = true - resp.Config.MaximumPaymentResults = uint32(ctx.Int("pmtnr")) + mcCfg.Config.MaximumPaymentResults = uint32(ctx.Int("pmtnr")) } - if ctx.IsSet("failrelax") { haveValue = true - resp.Config.MinimumFailureRelaxInterval = uint64(ctx.Duration( + mcCfg.Config.MinimumFailureRelaxInterval = uint64(ctx.Duration( "failrelax", ).Seconds()) } + // We switch between estimators and set corresponding configs. If + // estimator is not set, we ignore the values. + if ctx.IsSet("estimator") { + switch ctx.String("estimator") { + case routing.AprioriEstimatorName: + haveValue = true + + // If we switch from another estimator, initialize with + // default values. + if mcCfg.Config.Model != + routerrpc.MissionControlConfig_APRIORI { + + dCfg := routing.DefaultAprioriConfig() + aParams := &routerrpc.AprioriParameters{ + HalfLifeSeconds: uint64( + dCfg.PenaltyHalfLife.Seconds(), + ), + HopProbability: dCfg.AprioriHopProbability, //nolint:lll + Weight: dCfg.AprioriWeight, + } + + // We make sure the correct config is set. + mcCfg.Config.EstimatorConfig = + &routerrpc.MissionControlConfig_Apriori{ + Apriori: aParams, + } + } + + // We update all values for the apriori estimator. + mcCfg.Config.Model = routerrpc. + MissionControlConfig_APRIORI + + aCfg := mcCfg.Config.GetApriori() + if ctx.IsSet("apriorihalflife") { + aCfg.HalfLifeSeconds = uint64(ctx.Duration( + "apriorihalflife", + ).Seconds()) + } + + if ctx.IsSet("apriorihopprob") { + aCfg.HopProbability = ctx.Float64( + "apriorihopprob", + ) + } + + if ctx.IsSet("aprioriweight") { + aCfg.Weight = ctx.Float64("aprioriweight") + } + + case routing.BimodalEstimatorName: + haveValue = true + + // If we switch from another estimator, initialize with + // default values. + if mcCfg.Config.Model != + routerrpc.MissionControlConfig_BIMODAL { + + dCfg := routing.DefaultBimodalConfig() + bParams := &routerrpc.BimodalParameters{ + DecayTime: uint64( + dCfg.BimodalDecayTime.Seconds(), + ), + ScaleMsat: uint64( + dCfg.BimodalScaleMsat, + ), + NodeWeight: dCfg.BimodalNodeWeight, + } + + // We make sure the correct config is set. + mcCfg.Config.EstimatorConfig = + &routerrpc.MissionControlConfig_Bimodal{ + Bimodal: bParams, + } + } + + // We update all values for the bimodal estimator. + mcCfg.Config.Model = routerrpc. + MissionControlConfig_BIMODAL + + bCfg := mcCfg.Config.GetBimodal() + if ctx.IsSet("bimodaldecaytime") { + bCfg.DecayTime = uint64(ctx.Duration( + "bimodaldecaytime", + ).Seconds()) + } + + if ctx.IsSet("bimodalscale") { + bCfg.ScaleMsat = ctx.Uint64("bimodalscale") + } + + if ctx.IsSet("bimodalweight") { + bCfg.NodeWeight = ctx.Float64( + "bimodalweight", + ) + } + + default: + return fmt.Errorf("unknown estimator %v", + ctx.String("estimator")) + } + } + if !haveValue { return cli.ShowCommandHelp(ctx, "setmccfg") } _, err = client.SetMissionControlConfig( ctxc, &routerrpc.SetMissionControlConfigRequest{ - Config: resp.Config, + Config: mcCfg.Config, }, ) return err diff --git a/config.go b/config.go index 6e8b5eb9c..dca79551f 100644 --- a/config.go +++ b/config.go @@ -465,6 +465,9 @@ type Config struct { // ActiveNetParams contains parameters of the target chain. ActiveNetParams chainreg.BitcoinNetParams + + // Estimator is used to estimate routing probabilities. + Estimator routing.Estimator } // DefaultConfig returns all default values for the Config struct. diff --git a/docs/release-notes/release-notes-0.16.0.md b/docs/release-notes/release-notes-0.16.0.md index 7e2c1d5e4..0cac35fc5 100644 --- a/docs/release-notes/release-notes-0.16.0.md +++ b/docs/release-notes/release-notes-0.16.0.md @@ -408,6 +408,16 @@ in the lnwire package](https://github.com/lightningnetwork/lnd/pull/7303) * [Pathfinding takes capacity of edges into account to improve success probability estimation.](https://github.com/lightningnetwork/lnd/pull/6857) +* [A new probability model ("bimodal") is added which models channel based + liquidities within a probability theory framework.]( + https://github.com/lightningnetwork/lnd/pull/6815) + +## Configuration +* Note that [this pathfinding change](https://github.com/lightningnetwork/lnd/pull/6815) + introduces a breaking change in lnd.conf apriori parameters under the routing + section, see sample-lnd.conf for an updated configuration. The behavior of + `lncli setmccfg/getmccfg` is altered as well. + ### Tooling and documentation @@ -465,6 +475,7 @@ refactor the itest for code health and maintenance. * Andras Banki-Horvath * andreihod * Antoni Spaanderman +* bitromortac * Carla Kirk-Cohen * Carsten Otto * Chris Geihsler diff --git a/lnrpc/routerrpc/config.go b/lnrpc/routerrpc/config.go index ec036258f..9877556db 100644 --- a/lnrpc/routerrpc/config.go +++ b/lnrpc/routerrpc/config.go @@ -42,14 +42,23 @@ type Config struct { // DefaultConfig defines the config defaults. func DefaultConfig() *Config { defaultRoutingConfig := RoutingConfig{ - AprioriHopProbability: routing.DefaultAprioriHopProbability, - AprioriWeight: routing.DefaultAprioriWeight, - MinRouteProbability: routing.DefaultMinRouteProbability, - PenaltyHalfLife: routing.DefaultPenaltyHalfLife, - AttemptCost: routing.DefaultAttemptCost.ToSatoshis(), - AttemptCostPPM: routing.DefaultAttemptCostPPM, - MaxMcHistory: routing.DefaultMaxMcHistory, - McFlushInterval: routing.DefaultMcFlushInterval, + ProbabilityEstimatorType: routing.DefaultEstimator, + MinRouteProbability: routing.DefaultMinRouteProbability, + + AttemptCost: routing.DefaultAttemptCost.ToSatoshis(), + AttemptCostPPM: routing.DefaultAttemptCostPPM, + MaxMcHistory: routing.DefaultMaxMcHistory, + McFlushInterval: routing.DefaultMcFlushInterval, + AprioriConfig: &AprioriConfig{ + HopProbability: routing.DefaultAprioriHopProbability, + Weight: routing.DefaultAprioriWeight, + PenaltyHalfLife: routing.DefaultPenaltyHalfLife, + }, + BimodalConfig: &BimodalConfig{ + Scale: int64(routing.DefaultBimodalScaleMsat), + NodeWeight: routing.DefaultBimodalNodeWeight, + DecayTime: routing.DefaultBimodalDecayTime, + }, } return &Config{ @@ -60,13 +69,21 @@ func DefaultConfig() *Config { // GetRoutingConfig returns the routing config based on this sub server config. func GetRoutingConfig(cfg *Config) *RoutingConfig { return &RoutingConfig{ - AprioriHopProbability: cfg.AprioriHopProbability, - AprioriWeight: cfg.AprioriWeight, - MinRouteProbability: cfg.MinRouteProbability, - AttemptCost: cfg.AttemptCost, - AttemptCostPPM: cfg.AttemptCostPPM, - PenaltyHalfLife: cfg.PenaltyHalfLife, - MaxMcHistory: cfg.MaxMcHistory, - McFlushInterval: cfg.McFlushInterval, + ProbabilityEstimatorType: cfg.ProbabilityEstimatorType, + MinRouteProbability: cfg.MinRouteProbability, + AttemptCost: cfg.AttemptCost, + AttemptCostPPM: cfg.AttemptCostPPM, + MaxMcHistory: cfg.MaxMcHistory, + McFlushInterval: cfg.McFlushInterval, + AprioriConfig: &AprioriConfig{ + HopProbability: cfg.AprioriConfig.HopProbability, + Weight: cfg.AprioriConfig.Weight, + PenaltyHalfLife: cfg.AprioriConfig.PenaltyHalfLife, + }, + BimodalConfig: &BimodalConfig{ + Scale: cfg.BimodalConfig.Scale, + NodeWeight: cfg.BimodalConfig.NodeWeight, + DecayTime: cfg.BimodalConfig.DecayTime, + }, } } diff --git a/lnrpc/routerrpc/router.pb.go b/lnrpc/routerrpc/router.pb.go index f19cb0024..e81f09549 100644 --- a/lnrpc/routerrpc/router.pb.go +++ b/lnrpc/routerrpc/router.pb.go @@ -298,6 +298,52 @@ func (ChanStatusAction) EnumDescriptor() ([]byte, []int) { return file_routerrpc_router_proto_rawDescGZIP(), []int{3} } +type MissionControlConfig_ProbabilityModel int32 + +const ( + MissionControlConfig_APRIORI MissionControlConfig_ProbabilityModel = 0 + MissionControlConfig_BIMODAL MissionControlConfig_ProbabilityModel = 1 +) + +// Enum value maps for MissionControlConfig_ProbabilityModel. +var ( + MissionControlConfig_ProbabilityModel_name = map[int32]string{ + 0: "APRIORI", + 1: "BIMODAL", + } + MissionControlConfig_ProbabilityModel_value = map[string]int32{ + "APRIORI": 0, + "BIMODAL": 1, + } +) + +func (x MissionControlConfig_ProbabilityModel) Enum() *MissionControlConfig_ProbabilityModel { + p := new(MissionControlConfig_ProbabilityModel) + *p = x + return p +} + +func (x MissionControlConfig_ProbabilityModel) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (MissionControlConfig_ProbabilityModel) Descriptor() protoreflect.EnumDescriptor { + return file_routerrpc_router_proto_enumTypes[4].Descriptor() +} + +func (MissionControlConfig_ProbabilityModel) Type() protoreflect.EnumType { + return &file_routerrpc_router_proto_enumTypes[4] +} + +func (x MissionControlConfig_ProbabilityModel) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use MissionControlConfig_ProbabilityModel.Descriptor instead. +func (MissionControlConfig_ProbabilityModel) EnumDescriptor() ([]byte, []int) { + return file_routerrpc_router_proto_rawDescGZIP(), []int{19, 0} +} + type HtlcEvent_EventType int32 const ( @@ -334,11 +380,11 @@ func (x HtlcEvent_EventType) String() string { } func (HtlcEvent_EventType) Descriptor() protoreflect.EnumDescriptor { - return file_routerrpc_router_proto_enumTypes[4].Descriptor() + return file_routerrpc_router_proto_enumTypes[5].Descriptor() } func (HtlcEvent_EventType) Type() protoreflect.EnumType { - return &file_routerrpc_router_proto_enumTypes[4] + return &file_routerrpc_router_proto_enumTypes[5] } func (x HtlcEvent_EventType) Number() protoreflect.EnumNumber { @@ -347,7 +393,7 @@ func (x HtlcEvent_EventType) Number() protoreflect.EnumNumber { // Deprecated: Use HtlcEvent_EventType.Descriptor instead. func (HtlcEvent_EventType) EnumDescriptor() ([]byte, []int) { - return file_routerrpc_router_proto_rawDescGZIP(), []int{25, 0} + return file_routerrpc_router_proto_rawDescGZIP(), []int{27, 0} } type SendPaymentRequest struct { @@ -1590,29 +1636,46 @@ type MissionControlConfig struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // The amount of time mission control will take to restore a penalized node - // or channel back to 50% success probability, expressed in seconds. Setting - // this value to a higher value will penalize failures for longer, making - // mission control less likely to route through nodes and channels that we - // have previously recorded failures for. + // Deprecated, use AprioriParameters. The amount of time mission control will + // take to restore a penalized node or channel back to 50% success probability, + // expressed in seconds. Setting this value to a higher value will penalize + // failures for longer, making mission control less likely to route through + // nodes and channels that we have previously recorded failures for. + // + // Deprecated: Do not use. HalfLifeSeconds uint64 `protobuf:"varint,1,opt,name=half_life_seconds,json=halfLifeSeconds,proto3" json:"half_life_seconds,omitempty"` - // The probability of success mission control should assign to hop in a route - // where it has no other information available. Higher values will make mission - // control more willing to try hops that we have no information about, lower - // values will discourage trying these hops. + // Deprecated, use AprioriParameters. The probability of success mission + // control should assign to hop in a route where it has no other information + // available. Higher values will make mission control more willing to try hops + // that we have no information about, lower values will discourage trying these + // hops. + // + // Deprecated: Do not use. HopProbability float32 `protobuf:"fixed32,2,opt,name=hop_probability,json=hopProbability,proto3" json:"hop_probability,omitempty"` - // The importance that mission control should place on historical results, - // expressed as a value in [0;1]. Setting this value to 1 will ignore all - // historical payments and just use the hop probability to assess the - // probability of success for each hop. A zero value ignores hop probability - // completely and relies entirely on historical results, unless none are - // available. + // Deprecated, use AprioriParameters. The importance that mission control + // should place on historical results, expressed as a value in [0;1]. Setting + // this value to 1 will ignore all historical payments and just use the hop + // probability to assess the probability of success for each hop. A zero value + // ignores hop probability completely and relies entirely on historical + // results, unless none are available. + // + // Deprecated: Do not use. Weight float32 `protobuf:"fixed32,3,opt,name=weight,proto3" json:"weight,omitempty"` // The maximum number of payment results that mission control will store. MaximumPaymentResults uint32 `protobuf:"varint,4,opt,name=maximum_payment_results,json=maximumPaymentResults,proto3" json:"maximum_payment_results,omitempty"` // The minimum time that must have passed since the previously recorded failure // before we raise the failure amount. MinimumFailureRelaxInterval uint64 `protobuf:"varint,5,opt,name=minimum_failure_relax_interval,json=minimumFailureRelaxInterval,proto3" json:"minimum_failure_relax_interval,omitempty"` + // ProbabilityModel defines which probability estimator should be used in + // pathfinding. + Model MissionControlConfig_ProbabilityModel `protobuf:"varint,6,opt,name=Model,proto3,enum=routerrpc.MissionControlConfig_ProbabilityModel" json:"Model,omitempty"` + // EstimatorConfig is populated dependent on the estimator type. + // + // Types that are assignable to EstimatorConfig: + // + // *MissionControlConfig_Apriori + // *MissionControlConfig_Bimodal + EstimatorConfig isMissionControlConfig_EstimatorConfig `protobuf_oneof:"EstimatorConfig"` } func (x *MissionControlConfig) Reset() { @@ -1647,6 +1710,7 @@ func (*MissionControlConfig) Descriptor() ([]byte, []int) { return file_routerrpc_router_proto_rawDescGZIP(), []int{19} } +// Deprecated: Do not use. func (x *MissionControlConfig) GetHalfLifeSeconds() uint64 { if x != nil { return x.HalfLifeSeconds @@ -1654,6 +1718,7 @@ func (x *MissionControlConfig) GetHalfLifeSeconds() uint64 { return 0 } +// Deprecated: Do not use. func (x *MissionControlConfig) GetHopProbability() float32 { if x != nil { return x.HopProbability @@ -1661,6 +1726,7 @@ func (x *MissionControlConfig) GetHopProbability() float32 { return 0 } +// Deprecated: Do not use. func (x *MissionControlConfig) GetWeight() float32 { if x != nil { return x.Weight @@ -1682,6 +1748,204 @@ func (x *MissionControlConfig) GetMinimumFailureRelaxInterval() uint64 { return 0 } +func (x *MissionControlConfig) GetModel() MissionControlConfig_ProbabilityModel { + if x != nil { + return x.Model + } + return MissionControlConfig_APRIORI +} + +func (m *MissionControlConfig) GetEstimatorConfig() isMissionControlConfig_EstimatorConfig { + if m != nil { + return m.EstimatorConfig + } + return nil +} + +func (x *MissionControlConfig) GetApriori() *AprioriParameters { + if x, ok := x.GetEstimatorConfig().(*MissionControlConfig_Apriori); ok { + return x.Apriori + } + return nil +} + +func (x *MissionControlConfig) GetBimodal() *BimodalParameters { + if x, ok := x.GetEstimatorConfig().(*MissionControlConfig_Bimodal); ok { + return x.Bimodal + } + return nil +} + +type isMissionControlConfig_EstimatorConfig interface { + isMissionControlConfig_EstimatorConfig() +} + +type MissionControlConfig_Apriori struct { + Apriori *AprioriParameters `protobuf:"bytes,7,opt,name=apriori,proto3,oneof"` +} + +type MissionControlConfig_Bimodal struct { + Bimodal *BimodalParameters `protobuf:"bytes,8,opt,name=bimodal,proto3,oneof"` +} + +func (*MissionControlConfig_Apriori) isMissionControlConfig_EstimatorConfig() {} + +func (*MissionControlConfig_Bimodal) isMissionControlConfig_EstimatorConfig() {} + +type BimodalParameters struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // NodeWeight defines how strongly other previous forwardings on channels of a + // router should be taken into account when computing a channel's probability + // to route. The allowed values are in the range [0, 1], where a value of 0 + // means that only direct information about a channel is taken into account. + NodeWeight float64 `protobuf:"fixed64,1,opt,name=node_weight,json=nodeWeight,proto3" json:"node_weight,omitempty"` + // ScaleMsat describes the scale over which channels statistically have some + // liquidity left. The value determines how quickly the bimodal distribution + // drops off from the edges of a channel. A larger value (compared to typical + // channel capacities) means that the drop off is slow and that channel + // balances are distributed more uniformly. A small value leads to the + // assumption of very unbalanced channels. + ScaleMsat uint64 `protobuf:"varint,2,opt,name=scale_msat,json=scaleMsat,proto3" json:"scale_msat,omitempty"` + // DecayTime describes the information decay of knowledge about previous + // successes and failures in channels. The smaller the decay time, the quicker + // we forget about past forwardings. + DecayTime uint64 `protobuf:"varint,3,opt,name=decay_time,json=decayTime,proto3" json:"decay_time,omitempty"` +} + +func (x *BimodalParameters) Reset() { + *x = BimodalParameters{} + if protoimpl.UnsafeEnabled { + mi := &file_routerrpc_router_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *BimodalParameters) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BimodalParameters) ProtoMessage() {} + +func (x *BimodalParameters) ProtoReflect() protoreflect.Message { + mi := &file_routerrpc_router_proto_msgTypes[20] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BimodalParameters.ProtoReflect.Descriptor instead. +func (*BimodalParameters) Descriptor() ([]byte, []int) { + return file_routerrpc_router_proto_rawDescGZIP(), []int{20} +} + +func (x *BimodalParameters) GetNodeWeight() float64 { + if x != nil { + return x.NodeWeight + } + return 0 +} + +func (x *BimodalParameters) GetScaleMsat() uint64 { + if x != nil { + return x.ScaleMsat + } + return 0 +} + +func (x *BimodalParameters) GetDecayTime() uint64 { + if x != nil { + return x.DecayTime + } + return 0 +} + +type AprioriParameters struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The amount of time mission control will take to restore a penalized node + // or channel back to 50% success probability, expressed in seconds. Setting + // this value to a higher value will penalize failures for longer, making + // mission control less likely to route through nodes and channels that we + // have previously recorded failures for. + HalfLifeSeconds uint64 `protobuf:"varint,1,opt,name=half_life_seconds,json=halfLifeSeconds,proto3" json:"half_life_seconds,omitempty"` + // The probability of success mission control should assign to hop in a route + // where it has no other information available. Higher values will make mission + // control more willing to try hops that we have no information about, lower + // values will discourage trying these hops. + HopProbability float64 `protobuf:"fixed64,2,opt,name=hop_probability,json=hopProbability,proto3" json:"hop_probability,omitempty"` + // The importance that mission control should place on historical results, + // expressed as a value in [0;1]. Setting this value to 1 will ignore all + // historical payments and just use the hop probability to assess the + // probability of success for each hop. A zero value ignores hop probability + // completely and relies entirely on historical results, unless none are + // available. + Weight float64 `protobuf:"fixed64,3,opt,name=weight,proto3" json:"weight,omitempty"` +} + +func (x *AprioriParameters) Reset() { + *x = AprioriParameters{} + if protoimpl.UnsafeEnabled { + mi := &file_routerrpc_router_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AprioriParameters) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AprioriParameters) ProtoMessage() {} + +func (x *AprioriParameters) ProtoReflect() protoreflect.Message { + mi := &file_routerrpc_router_proto_msgTypes[21] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AprioriParameters.ProtoReflect.Descriptor instead. +func (*AprioriParameters) Descriptor() ([]byte, []int) { + return file_routerrpc_router_proto_rawDescGZIP(), []int{21} +} + +func (x *AprioriParameters) GetHalfLifeSeconds() uint64 { + if x != nil { + return x.HalfLifeSeconds + } + return 0 +} + +func (x *AprioriParameters) GetHopProbability() float64 { + if x != nil { + return x.HopProbability + } + return 0 +} + +func (x *AprioriParameters) GetWeight() float64 { + if x != nil { + return x.Weight + } + return 0 +} + type QueryProbabilityRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1698,7 +1962,7 @@ type QueryProbabilityRequest struct { func (x *QueryProbabilityRequest) Reset() { *x = QueryProbabilityRequest{} if protoimpl.UnsafeEnabled { - mi := &file_routerrpc_router_proto_msgTypes[20] + mi := &file_routerrpc_router_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1711,7 +1975,7 @@ func (x *QueryProbabilityRequest) String() string { func (*QueryProbabilityRequest) ProtoMessage() {} func (x *QueryProbabilityRequest) ProtoReflect() protoreflect.Message { - mi := &file_routerrpc_router_proto_msgTypes[20] + mi := &file_routerrpc_router_proto_msgTypes[22] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1724,7 +1988,7 @@ func (x *QueryProbabilityRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use QueryProbabilityRequest.ProtoReflect.Descriptor instead. func (*QueryProbabilityRequest) Descriptor() ([]byte, []int) { - return file_routerrpc_router_proto_rawDescGZIP(), []int{20} + return file_routerrpc_router_proto_rawDescGZIP(), []int{22} } func (x *QueryProbabilityRequest) GetFromNode() []byte { @@ -1762,7 +2026,7 @@ type QueryProbabilityResponse struct { func (x *QueryProbabilityResponse) Reset() { *x = QueryProbabilityResponse{} if protoimpl.UnsafeEnabled { - mi := &file_routerrpc_router_proto_msgTypes[21] + mi := &file_routerrpc_router_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1775,7 +2039,7 @@ func (x *QueryProbabilityResponse) String() string { func (*QueryProbabilityResponse) ProtoMessage() {} func (x *QueryProbabilityResponse) ProtoReflect() protoreflect.Message { - mi := &file_routerrpc_router_proto_msgTypes[21] + mi := &file_routerrpc_router_proto_msgTypes[23] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1788,7 +2052,7 @@ func (x *QueryProbabilityResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use QueryProbabilityResponse.ProtoReflect.Descriptor instead. func (*QueryProbabilityResponse) Descriptor() ([]byte, []int) { - return file_routerrpc_router_proto_rawDescGZIP(), []int{21} + return file_routerrpc_router_proto_rawDescGZIP(), []int{23} } func (x *QueryProbabilityResponse) GetProbability() float64 { @@ -1829,7 +2093,7 @@ type BuildRouteRequest struct { func (x *BuildRouteRequest) Reset() { *x = BuildRouteRequest{} if protoimpl.UnsafeEnabled { - mi := &file_routerrpc_router_proto_msgTypes[22] + mi := &file_routerrpc_router_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1842,7 +2106,7 @@ func (x *BuildRouteRequest) String() string { func (*BuildRouteRequest) ProtoMessage() {} func (x *BuildRouteRequest) ProtoReflect() protoreflect.Message { - mi := &file_routerrpc_router_proto_msgTypes[22] + mi := &file_routerrpc_router_proto_msgTypes[24] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1855,7 +2119,7 @@ func (x *BuildRouteRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use BuildRouteRequest.ProtoReflect.Descriptor instead. func (*BuildRouteRequest) Descriptor() ([]byte, []int) { - return file_routerrpc_router_proto_rawDescGZIP(), []int{22} + return file_routerrpc_router_proto_rawDescGZIP(), []int{24} } func (x *BuildRouteRequest) GetAmtMsat() int64 { @@ -1905,7 +2169,7 @@ type BuildRouteResponse struct { func (x *BuildRouteResponse) Reset() { *x = BuildRouteResponse{} if protoimpl.UnsafeEnabled { - mi := &file_routerrpc_router_proto_msgTypes[23] + mi := &file_routerrpc_router_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1918,7 +2182,7 @@ func (x *BuildRouteResponse) String() string { func (*BuildRouteResponse) ProtoMessage() {} func (x *BuildRouteResponse) ProtoReflect() protoreflect.Message { - mi := &file_routerrpc_router_proto_msgTypes[23] + mi := &file_routerrpc_router_proto_msgTypes[25] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1931,7 +2195,7 @@ func (x *BuildRouteResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use BuildRouteResponse.ProtoReflect.Descriptor instead. func (*BuildRouteResponse) Descriptor() ([]byte, []int) { - return file_routerrpc_router_proto_rawDescGZIP(), []int{23} + return file_routerrpc_router_proto_rawDescGZIP(), []int{25} } func (x *BuildRouteResponse) GetRoute() *lnrpc.Route { @@ -1950,7 +2214,7 @@ type SubscribeHtlcEventsRequest struct { func (x *SubscribeHtlcEventsRequest) Reset() { *x = SubscribeHtlcEventsRequest{} if protoimpl.UnsafeEnabled { - mi := &file_routerrpc_router_proto_msgTypes[24] + mi := &file_routerrpc_router_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1963,7 +2227,7 @@ func (x *SubscribeHtlcEventsRequest) String() string { func (*SubscribeHtlcEventsRequest) ProtoMessage() {} func (x *SubscribeHtlcEventsRequest) ProtoReflect() protoreflect.Message { - mi := &file_routerrpc_router_proto_msgTypes[24] + mi := &file_routerrpc_router_proto_msgTypes[26] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1976,7 +2240,7 @@ func (x *SubscribeHtlcEventsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SubscribeHtlcEventsRequest.ProtoReflect.Descriptor instead. func (*SubscribeHtlcEventsRequest) Descriptor() ([]byte, []int) { - return file_routerrpc_router_proto_rawDescGZIP(), []int{24} + return file_routerrpc_router_proto_rawDescGZIP(), []int{26} } // HtlcEvent contains the htlc event that was processed. These are served on a @@ -2021,7 +2285,7 @@ type HtlcEvent struct { func (x *HtlcEvent) Reset() { *x = HtlcEvent{} if protoimpl.UnsafeEnabled { - mi := &file_routerrpc_router_proto_msgTypes[25] + mi := &file_routerrpc_router_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2034,7 +2298,7 @@ func (x *HtlcEvent) String() string { func (*HtlcEvent) ProtoMessage() {} func (x *HtlcEvent) ProtoReflect() protoreflect.Message { - mi := &file_routerrpc_router_proto_msgTypes[25] + mi := &file_routerrpc_router_proto_msgTypes[27] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2047,7 +2311,7 @@ func (x *HtlcEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use HtlcEvent.ProtoReflect.Descriptor instead. func (*HtlcEvent) Descriptor() ([]byte, []int) { - return file_routerrpc_router_proto_rawDescGZIP(), []int{25} + return file_routerrpc_router_proto_rawDescGZIP(), []int{27} } func (x *HtlcEvent) GetIncomingChannelId() uint64 { @@ -2199,7 +2463,7 @@ type HtlcInfo struct { func (x *HtlcInfo) Reset() { *x = HtlcInfo{} if protoimpl.UnsafeEnabled { - mi := &file_routerrpc_router_proto_msgTypes[26] + mi := &file_routerrpc_router_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2212,7 +2476,7 @@ func (x *HtlcInfo) String() string { func (*HtlcInfo) ProtoMessage() {} func (x *HtlcInfo) ProtoReflect() protoreflect.Message { - mi := &file_routerrpc_router_proto_msgTypes[26] + mi := &file_routerrpc_router_proto_msgTypes[28] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2225,7 +2489,7 @@ func (x *HtlcInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use HtlcInfo.ProtoReflect.Descriptor instead. func (*HtlcInfo) Descriptor() ([]byte, []int) { - return file_routerrpc_router_proto_rawDescGZIP(), []int{26} + return file_routerrpc_router_proto_rawDescGZIP(), []int{28} } func (x *HtlcInfo) GetIncomingTimelock() uint32 { @@ -2268,7 +2532,7 @@ type ForwardEvent struct { func (x *ForwardEvent) Reset() { *x = ForwardEvent{} if protoimpl.UnsafeEnabled { - mi := &file_routerrpc_router_proto_msgTypes[27] + mi := &file_routerrpc_router_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2281,7 +2545,7 @@ func (x *ForwardEvent) String() string { func (*ForwardEvent) ProtoMessage() {} func (x *ForwardEvent) ProtoReflect() protoreflect.Message { - mi := &file_routerrpc_router_proto_msgTypes[27] + mi := &file_routerrpc_router_proto_msgTypes[29] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2294,7 +2558,7 @@ func (x *ForwardEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use ForwardEvent.ProtoReflect.Descriptor instead. func (*ForwardEvent) Descriptor() ([]byte, []int) { - return file_routerrpc_router_proto_rawDescGZIP(), []int{27} + return file_routerrpc_router_proto_rawDescGZIP(), []int{29} } func (x *ForwardEvent) GetInfo() *HtlcInfo { @@ -2313,7 +2577,7 @@ type ForwardFailEvent struct { func (x *ForwardFailEvent) Reset() { *x = ForwardFailEvent{} if protoimpl.UnsafeEnabled { - mi := &file_routerrpc_router_proto_msgTypes[28] + mi := &file_routerrpc_router_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2326,7 +2590,7 @@ func (x *ForwardFailEvent) String() string { func (*ForwardFailEvent) ProtoMessage() {} func (x *ForwardFailEvent) ProtoReflect() protoreflect.Message { - mi := &file_routerrpc_router_proto_msgTypes[28] + mi := &file_routerrpc_router_proto_msgTypes[30] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2339,7 +2603,7 @@ func (x *ForwardFailEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use ForwardFailEvent.ProtoReflect.Descriptor instead. func (*ForwardFailEvent) Descriptor() ([]byte, []int) { - return file_routerrpc_router_proto_rawDescGZIP(), []int{28} + return file_routerrpc_router_proto_rawDescGZIP(), []int{30} } type SettleEvent struct { @@ -2354,7 +2618,7 @@ type SettleEvent struct { func (x *SettleEvent) Reset() { *x = SettleEvent{} if protoimpl.UnsafeEnabled { - mi := &file_routerrpc_router_proto_msgTypes[29] + mi := &file_routerrpc_router_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2367,7 +2631,7 @@ func (x *SettleEvent) String() string { func (*SettleEvent) ProtoMessage() {} func (x *SettleEvent) ProtoReflect() protoreflect.Message { - mi := &file_routerrpc_router_proto_msgTypes[29] + mi := &file_routerrpc_router_proto_msgTypes[31] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2380,7 +2644,7 @@ func (x *SettleEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use SettleEvent.ProtoReflect.Descriptor instead. func (*SettleEvent) Descriptor() ([]byte, []int) { - return file_routerrpc_router_proto_rawDescGZIP(), []int{29} + return file_routerrpc_router_proto_rawDescGZIP(), []int{31} } func (x *SettleEvent) GetPreimage() []byte { @@ -2402,7 +2666,7 @@ type FinalHtlcEvent struct { func (x *FinalHtlcEvent) Reset() { *x = FinalHtlcEvent{} if protoimpl.UnsafeEnabled { - mi := &file_routerrpc_router_proto_msgTypes[30] + mi := &file_routerrpc_router_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2415,7 +2679,7 @@ func (x *FinalHtlcEvent) String() string { func (*FinalHtlcEvent) ProtoMessage() {} func (x *FinalHtlcEvent) ProtoReflect() protoreflect.Message { - mi := &file_routerrpc_router_proto_msgTypes[30] + mi := &file_routerrpc_router_proto_msgTypes[32] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2428,7 +2692,7 @@ func (x *FinalHtlcEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use FinalHtlcEvent.ProtoReflect.Descriptor instead. func (*FinalHtlcEvent) Descriptor() ([]byte, []int) { - return file_routerrpc_router_proto_rawDescGZIP(), []int{30} + return file_routerrpc_router_proto_rawDescGZIP(), []int{32} } func (x *FinalHtlcEvent) GetSettled() bool { @@ -2454,7 +2718,7 @@ type SubscribedEvent struct { func (x *SubscribedEvent) Reset() { *x = SubscribedEvent{} if protoimpl.UnsafeEnabled { - mi := &file_routerrpc_router_proto_msgTypes[31] + mi := &file_routerrpc_router_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2467,7 +2731,7 @@ func (x *SubscribedEvent) String() string { func (*SubscribedEvent) ProtoMessage() {} func (x *SubscribedEvent) ProtoReflect() protoreflect.Message { - mi := &file_routerrpc_router_proto_msgTypes[31] + mi := &file_routerrpc_router_proto_msgTypes[33] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2480,7 +2744,7 @@ func (x *SubscribedEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use SubscribedEvent.ProtoReflect.Descriptor instead. func (*SubscribedEvent) Descriptor() ([]byte, []int) { - return file_routerrpc_router_proto_rawDescGZIP(), []int{31} + return file_routerrpc_router_proto_rawDescGZIP(), []int{33} } type LinkFailEvent struct { @@ -2503,7 +2767,7 @@ type LinkFailEvent struct { func (x *LinkFailEvent) Reset() { *x = LinkFailEvent{} if protoimpl.UnsafeEnabled { - mi := &file_routerrpc_router_proto_msgTypes[32] + mi := &file_routerrpc_router_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2516,7 +2780,7 @@ func (x *LinkFailEvent) String() string { func (*LinkFailEvent) ProtoMessage() {} func (x *LinkFailEvent) ProtoReflect() protoreflect.Message { - mi := &file_routerrpc_router_proto_msgTypes[32] + mi := &file_routerrpc_router_proto_msgTypes[34] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2529,7 +2793,7 @@ func (x *LinkFailEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use LinkFailEvent.ProtoReflect.Descriptor instead. func (*LinkFailEvent) Descriptor() ([]byte, []int) { - return file_routerrpc_router_proto_rawDescGZIP(), []int{32} + return file_routerrpc_router_proto_rawDescGZIP(), []int{34} } func (x *LinkFailEvent) GetInfo() *HtlcInfo { @@ -2576,7 +2840,7 @@ type PaymentStatus struct { func (x *PaymentStatus) Reset() { *x = PaymentStatus{} if protoimpl.UnsafeEnabled { - mi := &file_routerrpc_router_proto_msgTypes[33] + mi := &file_routerrpc_router_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2589,7 +2853,7 @@ func (x *PaymentStatus) String() string { func (*PaymentStatus) ProtoMessage() {} func (x *PaymentStatus) ProtoReflect() protoreflect.Message { - mi := &file_routerrpc_router_proto_msgTypes[33] + mi := &file_routerrpc_router_proto_msgTypes[35] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2602,7 +2866,7 @@ func (x *PaymentStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use PaymentStatus.ProtoReflect.Descriptor instead. func (*PaymentStatus) Descriptor() ([]byte, []int) { - return file_routerrpc_router_proto_rawDescGZIP(), []int{33} + return file_routerrpc_router_proto_rawDescGZIP(), []int{35} } func (x *PaymentStatus) GetState() PaymentState { @@ -2640,7 +2904,7 @@ type CircuitKey struct { func (x *CircuitKey) Reset() { *x = CircuitKey{} if protoimpl.UnsafeEnabled { - mi := &file_routerrpc_router_proto_msgTypes[34] + mi := &file_routerrpc_router_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2653,7 +2917,7 @@ func (x *CircuitKey) String() string { func (*CircuitKey) ProtoMessage() {} func (x *CircuitKey) ProtoReflect() protoreflect.Message { - mi := &file_routerrpc_router_proto_msgTypes[34] + mi := &file_routerrpc_router_proto_msgTypes[36] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2666,7 +2930,7 @@ func (x *CircuitKey) ProtoReflect() protoreflect.Message { // Deprecated: Use CircuitKey.ProtoReflect.Descriptor instead. func (*CircuitKey) Descriptor() ([]byte, []int) { - return file_routerrpc_router_proto_rawDescGZIP(), []int{34} + return file_routerrpc_router_proto_rawDescGZIP(), []int{36} } func (x *CircuitKey) GetChanId() uint64 { @@ -2719,7 +2983,7 @@ type ForwardHtlcInterceptRequest struct { func (x *ForwardHtlcInterceptRequest) Reset() { *x = ForwardHtlcInterceptRequest{} if protoimpl.UnsafeEnabled { - mi := &file_routerrpc_router_proto_msgTypes[35] + mi := &file_routerrpc_router_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2732,7 +2996,7 @@ func (x *ForwardHtlcInterceptRequest) String() string { func (*ForwardHtlcInterceptRequest) ProtoMessage() {} func (x *ForwardHtlcInterceptRequest) ProtoReflect() protoreflect.Message { - mi := &file_routerrpc_router_proto_msgTypes[35] + mi := &file_routerrpc_router_proto_msgTypes[37] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2745,7 +3009,7 @@ func (x *ForwardHtlcInterceptRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ForwardHtlcInterceptRequest.ProtoReflect.Descriptor instead. func (*ForwardHtlcInterceptRequest) Descriptor() ([]byte, []int) { - return file_routerrpc_router_proto_rawDescGZIP(), []int{35} + return file_routerrpc_router_proto_rawDescGZIP(), []int{37} } func (x *ForwardHtlcInterceptRequest) GetIncomingCircuitKey() *CircuitKey { @@ -2855,7 +3119,7 @@ type ForwardHtlcInterceptResponse struct { func (x *ForwardHtlcInterceptResponse) Reset() { *x = ForwardHtlcInterceptResponse{} if protoimpl.UnsafeEnabled { - mi := &file_routerrpc_router_proto_msgTypes[36] + mi := &file_routerrpc_router_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2868,7 +3132,7 @@ func (x *ForwardHtlcInterceptResponse) String() string { func (*ForwardHtlcInterceptResponse) ProtoMessage() {} func (x *ForwardHtlcInterceptResponse) ProtoReflect() protoreflect.Message { - mi := &file_routerrpc_router_proto_msgTypes[36] + mi := &file_routerrpc_router_proto_msgTypes[38] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2881,7 +3145,7 @@ func (x *ForwardHtlcInterceptResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ForwardHtlcInterceptResponse.ProtoReflect.Descriptor instead. func (*ForwardHtlcInterceptResponse) Descriptor() ([]byte, []int) { - return file_routerrpc_router_proto_rawDescGZIP(), []int{36} + return file_routerrpc_router_proto_rawDescGZIP(), []int{38} } func (x *ForwardHtlcInterceptResponse) GetIncomingCircuitKey() *CircuitKey { @@ -2931,7 +3195,7 @@ type UpdateChanStatusRequest struct { func (x *UpdateChanStatusRequest) Reset() { *x = UpdateChanStatusRequest{} if protoimpl.UnsafeEnabled { - mi := &file_routerrpc_router_proto_msgTypes[37] + mi := &file_routerrpc_router_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2944,7 +3208,7 @@ func (x *UpdateChanStatusRequest) String() string { func (*UpdateChanStatusRequest) ProtoMessage() {} func (x *UpdateChanStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_routerrpc_router_proto_msgTypes[37] + mi := &file_routerrpc_router_proto_msgTypes[39] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2957,7 +3221,7 @@ func (x *UpdateChanStatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateChanStatusRequest.ProtoReflect.Descriptor instead. func (*UpdateChanStatusRequest) Descriptor() ([]byte, []int) { - return file_routerrpc_router_proto_rawDescGZIP(), []int{37} + return file_routerrpc_router_proto_rawDescGZIP(), []int{39} } func (x *UpdateChanStatusRequest) GetChanPoint() *lnrpc.ChannelPoint { @@ -2983,7 +3247,7 @@ type UpdateChanStatusResponse struct { func (x *UpdateChanStatusResponse) Reset() { *x = UpdateChanStatusResponse{} if protoimpl.UnsafeEnabled { - mi := &file_routerrpc_router_proto_msgTypes[38] + mi := &file_routerrpc_router_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2996,7 +3260,7 @@ func (x *UpdateChanStatusResponse) String() string { func (*UpdateChanStatusResponse) ProtoMessage() {} func (x *UpdateChanStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_routerrpc_router_proto_msgTypes[38] + mi := &file_routerrpc_router_proto_msgTypes[40] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3009,7 +3273,7 @@ func (x *UpdateChanStatusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateChanStatusResponse.ProtoReflect.Descriptor instead. func (*UpdateChanStatusResponse) Descriptor() ([]byte, []int) { - return file_routerrpc_router_proto_rawDescGZIP(), []int{38} + return file_routerrpc_router_proto_rawDescGZIP(), []int{40} } var File_routerrpc_router_proto protoreflect.FileDescriptor @@ -3175,23 +3439,55 @@ var file_routerrpc_router_proto_rawDesc = []byte{ 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x21, 0x0a, 0x1f, 0x53, 0x65, 0x74, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x80, 0x02, 0x0a, 0x14, 0x4d, 0x69, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x89, 0x04, 0x0a, 0x14, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x12, 0x2a, 0x0a, 0x11, 0x68, 0x61, 0x6c, 0x66, 0x5f, 0x6c, 0x69, 0x66, 0x65, 0x5f, 0x73, - 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0f, 0x68, 0x61, - 0x6c, 0x66, 0x4c, 0x69, 0x66, 0x65, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x12, 0x27, 0x0a, - 0x0f, 0x68, 0x6f, 0x70, 0x5f, 0x70, 0x72, 0x6f, 0x62, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x02, 0x52, 0x0e, 0x68, 0x6f, 0x70, 0x50, 0x72, 0x6f, 0x62, 0x61, - 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x77, 0x65, 0x69, 0x67, 0x68, 0x74, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x02, 0x52, 0x06, 0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x36, - 0x0a, 0x17, 0x6d, 0x61, 0x78, 0x69, 0x6d, 0x75, 0x6d, 0x5f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, - 0x74, 0x5f, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, - 0x15, 0x6d, 0x61, 0x78, 0x69, 0x6d, 0x75, 0x6d, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, - 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x12, 0x43, 0x0a, 0x1e, 0x6d, 0x69, 0x6e, 0x69, 0x6d, 0x75, - 0x6d, 0x5f, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x5f, 0x72, 0x65, 0x6c, 0x61, 0x78, 0x5f, - 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x1b, - 0x6d, 0x69, 0x6e, 0x69, 0x6d, 0x75, 0x6d, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x52, 0x65, - 0x6c, 0x61, 0x78, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x22, 0x6a, 0x0a, 0x17, 0x51, + 0x67, 0x12, 0x2e, 0x0a, 0x11, 0x68, 0x61, 0x6c, 0x66, 0x5f, 0x6c, 0x69, 0x66, 0x65, 0x5f, 0x73, + 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x42, 0x02, 0x18, 0x01, + 0x52, 0x0f, 0x68, 0x61, 0x6c, 0x66, 0x4c, 0x69, 0x66, 0x65, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, + 0x73, 0x12, 0x2b, 0x0a, 0x0f, 0x68, 0x6f, 0x70, 0x5f, 0x70, 0x72, 0x6f, 0x62, 0x61, 0x62, 0x69, + 0x6c, 0x69, 0x74, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x02, 0x42, 0x02, 0x18, 0x01, 0x52, 0x0e, + 0x68, 0x6f, 0x70, 0x50, 0x72, 0x6f, 0x62, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x12, 0x1a, + 0x0a, 0x06, 0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x02, 0x42, 0x02, + 0x18, 0x01, 0x52, 0x06, 0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x36, 0x0a, 0x17, 0x6d, 0x61, + 0x78, 0x69, 0x6d, 0x75, 0x6d, 0x5f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x72, 0x65, + 0x73, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x15, 0x6d, 0x61, 0x78, + 0x69, 0x6d, 0x75, 0x6d, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x75, 0x6c, + 0x74, 0x73, 0x12, 0x43, 0x0a, 0x1e, 0x6d, 0x69, 0x6e, 0x69, 0x6d, 0x75, 0x6d, 0x5f, 0x66, 0x61, + 0x69, 0x6c, 0x75, 0x72, 0x65, 0x5f, 0x72, 0x65, 0x6c, 0x61, 0x78, 0x5f, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x76, 0x61, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x1b, 0x6d, 0x69, 0x6e, 0x69, + 0x6d, 0x75, 0x6d, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x52, 0x65, 0x6c, 0x61, 0x78, 0x49, + 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x12, 0x46, 0x0a, 0x05, 0x4d, 0x6f, 0x64, 0x65, 0x6c, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x30, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, + 0x70, 0x63, 0x2e, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, + 0x6c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x50, 0x72, 0x6f, 0x62, 0x61, 0x62, 0x69, 0x6c, + 0x69, 0x74, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x52, 0x05, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x12, + 0x38, 0x0a, 0x07, 0x61, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x69, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1c, 0x2e, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x70, 0x72, + 0x69, 0x6f, 0x72, 0x69, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x48, 0x00, + 0x52, 0x07, 0x61, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x69, 0x12, 0x38, 0x0a, 0x07, 0x62, 0x69, 0x6d, + 0x6f, 0x64, 0x61, 0x6c, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x72, 0x6f, 0x75, + 0x74, 0x65, 0x72, 0x72, 0x70, 0x63, 0x2e, 0x42, 0x69, 0x6d, 0x6f, 0x64, 0x61, 0x6c, 0x50, 0x61, + 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x48, 0x00, 0x52, 0x07, 0x62, 0x69, 0x6d, 0x6f, + 0x64, 0x61, 0x6c, 0x22, 0x2c, 0x0a, 0x10, 0x50, 0x72, 0x6f, 0x62, 0x61, 0x62, 0x69, 0x6c, 0x69, + 0x74, 0x79, 0x4d, 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x41, 0x50, 0x52, 0x49, 0x4f, + 0x52, 0x49, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x42, 0x49, 0x4d, 0x4f, 0x44, 0x41, 0x4c, 0x10, + 0x01, 0x42, 0x11, 0x0a, 0x0f, 0x45, 0x73, 0x74, 0x69, 0x6d, 0x61, 0x74, 0x6f, 0x72, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x22, 0x72, 0x0a, 0x11, 0x42, 0x69, 0x6d, 0x6f, 0x64, 0x61, 0x6c, 0x50, + 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x6e, 0x6f, 0x64, + 0x65, 0x5f, 0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x01, 0x52, 0x0a, + 0x6e, 0x6f, 0x64, 0x65, 0x57, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x63, + 0x61, 0x6c, 0x65, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, + 0x73, 0x63, 0x61, 0x6c, 0x65, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x64, 0x65, 0x63, + 0x61, 0x79, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x64, + 0x65, 0x63, 0x61, 0x79, 0x54, 0x69, 0x6d, 0x65, 0x22, 0x80, 0x01, 0x0a, 0x11, 0x41, 0x70, 0x72, + 0x69, 0x6f, 0x72, 0x69, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, 0x2a, + 0x0a, 0x11, 0x68, 0x61, 0x6c, 0x66, 0x5f, 0x6c, 0x69, 0x66, 0x65, 0x5f, 0x73, 0x65, 0x63, 0x6f, + 0x6e, 0x64, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0f, 0x68, 0x61, 0x6c, 0x66, 0x4c, + 0x69, 0x66, 0x65, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x12, 0x27, 0x0a, 0x0f, 0x68, 0x6f, + 0x70, 0x5f, 0x70, 0x72, 0x6f, 0x62, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x01, 0x52, 0x0e, 0x68, 0x6f, 0x70, 0x50, 0x72, 0x6f, 0x62, 0x61, 0x62, 0x69, 0x6c, + 0x69, 0x74, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x01, 0x52, 0x06, 0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 0x22, 0x6a, 0x0a, 0x17, 0x51, 0x75, 0x65, 0x72, 0x79, 0x50, 0x72, 0x6f, 0x62, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x72, 0x6f, 0x6d, 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x66, 0x72, 0x6f, 0x6d, 0x4e, @@ -3560,138 +3856,144 @@ func file_routerrpc_router_proto_rawDescGZIP() []byte { return file_routerrpc_router_proto_rawDescData } -var file_routerrpc_router_proto_enumTypes = make([]protoimpl.EnumInfo, 5) -var file_routerrpc_router_proto_msgTypes = make([]protoimpl.MessageInfo, 41) +var file_routerrpc_router_proto_enumTypes = make([]protoimpl.EnumInfo, 6) +var file_routerrpc_router_proto_msgTypes = make([]protoimpl.MessageInfo, 43) var file_routerrpc_router_proto_goTypes = []interface{}{ - (FailureDetail)(0), // 0: routerrpc.FailureDetail - (PaymentState)(0), // 1: routerrpc.PaymentState - (ResolveHoldForwardAction)(0), // 2: routerrpc.ResolveHoldForwardAction - (ChanStatusAction)(0), // 3: routerrpc.ChanStatusAction - (HtlcEvent_EventType)(0), // 4: routerrpc.HtlcEvent.EventType - (*SendPaymentRequest)(nil), // 5: routerrpc.SendPaymentRequest - (*TrackPaymentRequest)(nil), // 6: routerrpc.TrackPaymentRequest - (*TrackPaymentsRequest)(nil), // 7: routerrpc.TrackPaymentsRequest - (*RouteFeeRequest)(nil), // 8: routerrpc.RouteFeeRequest - (*RouteFeeResponse)(nil), // 9: routerrpc.RouteFeeResponse - (*SendToRouteRequest)(nil), // 10: routerrpc.SendToRouteRequest - (*SendToRouteResponse)(nil), // 11: routerrpc.SendToRouteResponse - (*ResetMissionControlRequest)(nil), // 12: routerrpc.ResetMissionControlRequest - (*ResetMissionControlResponse)(nil), // 13: routerrpc.ResetMissionControlResponse - (*QueryMissionControlRequest)(nil), // 14: routerrpc.QueryMissionControlRequest - (*QueryMissionControlResponse)(nil), // 15: routerrpc.QueryMissionControlResponse - (*XImportMissionControlRequest)(nil), // 16: routerrpc.XImportMissionControlRequest - (*XImportMissionControlResponse)(nil), // 17: routerrpc.XImportMissionControlResponse - (*PairHistory)(nil), // 18: routerrpc.PairHistory - (*PairData)(nil), // 19: routerrpc.PairData - (*GetMissionControlConfigRequest)(nil), // 20: routerrpc.GetMissionControlConfigRequest - (*GetMissionControlConfigResponse)(nil), // 21: routerrpc.GetMissionControlConfigResponse - (*SetMissionControlConfigRequest)(nil), // 22: routerrpc.SetMissionControlConfigRequest - (*SetMissionControlConfigResponse)(nil), // 23: routerrpc.SetMissionControlConfigResponse - (*MissionControlConfig)(nil), // 24: routerrpc.MissionControlConfig - (*QueryProbabilityRequest)(nil), // 25: routerrpc.QueryProbabilityRequest - (*QueryProbabilityResponse)(nil), // 26: routerrpc.QueryProbabilityResponse - (*BuildRouteRequest)(nil), // 27: routerrpc.BuildRouteRequest - (*BuildRouteResponse)(nil), // 28: routerrpc.BuildRouteResponse - (*SubscribeHtlcEventsRequest)(nil), // 29: routerrpc.SubscribeHtlcEventsRequest - (*HtlcEvent)(nil), // 30: routerrpc.HtlcEvent - (*HtlcInfo)(nil), // 31: routerrpc.HtlcInfo - (*ForwardEvent)(nil), // 32: routerrpc.ForwardEvent - (*ForwardFailEvent)(nil), // 33: routerrpc.ForwardFailEvent - (*SettleEvent)(nil), // 34: routerrpc.SettleEvent - (*FinalHtlcEvent)(nil), // 35: routerrpc.FinalHtlcEvent - (*SubscribedEvent)(nil), // 36: routerrpc.SubscribedEvent - (*LinkFailEvent)(nil), // 37: routerrpc.LinkFailEvent - (*PaymentStatus)(nil), // 38: routerrpc.PaymentStatus - (*CircuitKey)(nil), // 39: routerrpc.CircuitKey - (*ForwardHtlcInterceptRequest)(nil), // 40: routerrpc.ForwardHtlcInterceptRequest - (*ForwardHtlcInterceptResponse)(nil), // 41: routerrpc.ForwardHtlcInterceptResponse - (*UpdateChanStatusRequest)(nil), // 42: routerrpc.UpdateChanStatusRequest - (*UpdateChanStatusResponse)(nil), // 43: routerrpc.UpdateChanStatusResponse - nil, // 44: routerrpc.SendPaymentRequest.DestCustomRecordsEntry - nil, // 45: routerrpc.ForwardHtlcInterceptRequest.CustomRecordsEntry - (*lnrpc.RouteHint)(nil), // 46: lnrpc.RouteHint - (lnrpc.FeatureBit)(0), // 47: lnrpc.FeatureBit - (*lnrpc.Route)(nil), // 48: lnrpc.Route - (*lnrpc.Failure)(nil), // 49: lnrpc.Failure - (lnrpc.Failure_FailureCode)(0), // 50: lnrpc.Failure.FailureCode - (*lnrpc.HTLCAttempt)(nil), // 51: lnrpc.HTLCAttempt - (*lnrpc.ChannelPoint)(nil), // 52: lnrpc.ChannelPoint - (*lnrpc.Payment)(nil), // 53: lnrpc.Payment + (FailureDetail)(0), // 0: routerrpc.FailureDetail + (PaymentState)(0), // 1: routerrpc.PaymentState + (ResolveHoldForwardAction)(0), // 2: routerrpc.ResolveHoldForwardAction + (ChanStatusAction)(0), // 3: routerrpc.ChanStatusAction + (MissionControlConfig_ProbabilityModel)(0), // 4: routerrpc.MissionControlConfig.ProbabilityModel + (HtlcEvent_EventType)(0), // 5: routerrpc.HtlcEvent.EventType + (*SendPaymentRequest)(nil), // 6: routerrpc.SendPaymentRequest + (*TrackPaymentRequest)(nil), // 7: routerrpc.TrackPaymentRequest + (*TrackPaymentsRequest)(nil), // 8: routerrpc.TrackPaymentsRequest + (*RouteFeeRequest)(nil), // 9: routerrpc.RouteFeeRequest + (*RouteFeeResponse)(nil), // 10: routerrpc.RouteFeeResponse + (*SendToRouteRequest)(nil), // 11: routerrpc.SendToRouteRequest + (*SendToRouteResponse)(nil), // 12: routerrpc.SendToRouteResponse + (*ResetMissionControlRequest)(nil), // 13: routerrpc.ResetMissionControlRequest + (*ResetMissionControlResponse)(nil), // 14: routerrpc.ResetMissionControlResponse + (*QueryMissionControlRequest)(nil), // 15: routerrpc.QueryMissionControlRequest + (*QueryMissionControlResponse)(nil), // 16: routerrpc.QueryMissionControlResponse + (*XImportMissionControlRequest)(nil), // 17: routerrpc.XImportMissionControlRequest + (*XImportMissionControlResponse)(nil), // 18: routerrpc.XImportMissionControlResponse + (*PairHistory)(nil), // 19: routerrpc.PairHistory + (*PairData)(nil), // 20: routerrpc.PairData + (*GetMissionControlConfigRequest)(nil), // 21: routerrpc.GetMissionControlConfigRequest + (*GetMissionControlConfigResponse)(nil), // 22: routerrpc.GetMissionControlConfigResponse + (*SetMissionControlConfigRequest)(nil), // 23: routerrpc.SetMissionControlConfigRequest + (*SetMissionControlConfigResponse)(nil), // 24: routerrpc.SetMissionControlConfigResponse + (*MissionControlConfig)(nil), // 25: routerrpc.MissionControlConfig + (*BimodalParameters)(nil), // 26: routerrpc.BimodalParameters + (*AprioriParameters)(nil), // 27: routerrpc.AprioriParameters + (*QueryProbabilityRequest)(nil), // 28: routerrpc.QueryProbabilityRequest + (*QueryProbabilityResponse)(nil), // 29: routerrpc.QueryProbabilityResponse + (*BuildRouteRequest)(nil), // 30: routerrpc.BuildRouteRequest + (*BuildRouteResponse)(nil), // 31: routerrpc.BuildRouteResponse + (*SubscribeHtlcEventsRequest)(nil), // 32: routerrpc.SubscribeHtlcEventsRequest + (*HtlcEvent)(nil), // 33: routerrpc.HtlcEvent + (*HtlcInfo)(nil), // 34: routerrpc.HtlcInfo + (*ForwardEvent)(nil), // 35: routerrpc.ForwardEvent + (*ForwardFailEvent)(nil), // 36: routerrpc.ForwardFailEvent + (*SettleEvent)(nil), // 37: routerrpc.SettleEvent + (*FinalHtlcEvent)(nil), // 38: routerrpc.FinalHtlcEvent + (*SubscribedEvent)(nil), // 39: routerrpc.SubscribedEvent + (*LinkFailEvent)(nil), // 40: routerrpc.LinkFailEvent + (*PaymentStatus)(nil), // 41: routerrpc.PaymentStatus + (*CircuitKey)(nil), // 42: routerrpc.CircuitKey + (*ForwardHtlcInterceptRequest)(nil), // 43: routerrpc.ForwardHtlcInterceptRequest + (*ForwardHtlcInterceptResponse)(nil), // 44: routerrpc.ForwardHtlcInterceptResponse + (*UpdateChanStatusRequest)(nil), // 45: routerrpc.UpdateChanStatusRequest + (*UpdateChanStatusResponse)(nil), // 46: routerrpc.UpdateChanStatusResponse + nil, // 47: routerrpc.SendPaymentRequest.DestCustomRecordsEntry + nil, // 48: routerrpc.ForwardHtlcInterceptRequest.CustomRecordsEntry + (*lnrpc.RouteHint)(nil), // 49: lnrpc.RouteHint + (lnrpc.FeatureBit)(0), // 50: lnrpc.FeatureBit + (*lnrpc.Route)(nil), // 51: lnrpc.Route + (*lnrpc.Failure)(nil), // 52: lnrpc.Failure + (lnrpc.Failure_FailureCode)(0), // 53: lnrpc.Failure.FailureCode + (*lnrpc.HTLCAttempt)(nil), // 54: lnrpc.HTLCAttempt + (*lnrpc.ChannelPoint)(nil), // 55: lnrpc.ChannelPoint + (*lnrpc.Payment)(nil), // 56: lnrpc.Payment } var file_routerrpc_router_proto_depIdxs = []int32{ - 46, // 0: routerrpc.SendPaymentRequest.route_hints:type_name -> lnrpc.RouteHint - 44, // 1: routerrpc.SendPaymentRequest.dest_custom_records:type_name -> routerrpc.SendPaymentRequest.DestCustomRecordsEntry - 47, // 2: routerrpc.SendPaymentRequest.dest_features:type_name -> lnrpc.FeatureBit - 48, // 3: routerrpc.SendToRouteRequest.route:type_name -> lnrpc.Route - 49, // 4: routerrpc.SendToRouteResponse.failure:type_name -> lnrpc.Failure - 18, // 5: routerrpc.QueryMissionControlResponse.pairs:type_name -> routerrpc.PairHistory - 18, // 6: routerrpc.XImportMissionControlRequest.pairs:type_name -> routerrpc.PairHistory - 19, // 7: routerrpc.PairHistory.history:type_name -> routerrpc.PairData - 24, // 8: routerrpc.GetMissionControlConfigResponse.config:type_name -> routerrpc.MissionControlConfig - 24, // 9: routerrpc.SetMissionControlConfigRequest.config:type_name -> routerrpc.MissionControlConfig - 19, // 10: routerrpc.QueryProbabilityResponse.history:type_name -> routerrpc.PairData - 48, // 11: routerrpc.BuildRouteResponse.route:type_name -> lnrpc.Route - 4, // 12: routerrpc.HtlcEvent.event_type:type_name -> routerrpc.HtlcEvent.EventType - 32, // 13: routerrpc.HtlcEvent.forward_event:type_name -> routerrpc.ForwardEvent - 33, // 14: routerrpc.HtlcEvent.forward_fail_event:type_name -> routerrpc.ForwardFailEvent - 34, // 15: routerrpc.HtlcEvent.settle_event:type_name -> routerrpc.SettleEvent - 37, // 16: routerrpc.HtlcEvent.link_fail_event:type_name -> routerrpc.LinkFailEvent - 36, // 17: routerrpc.HtlcEvent.subscribed_event:type_name -> routerrpc.SubscribedEvent - 35, // 18: routerrpc.HtlcEvent.final_htlc_event:type_name -> routerrpc.FinalHtlcEvent - 31, // 19: routerrpc.ForwardEvent.info:type_name -> routerrpc.HtlcInfo - 31, // 20: routerrpc.LinkFailEvent.info:type_name -> routerrpc.HtlcInfo - 50, // 21: routerrpc.LinkFailEvent.wire_failure:type_name -> lnrpc.Failure.FailureCode - 0, // 22: routerrpc.LinkFailEvent.failure_detail:type_name -> routerrpc.FailureDetail - 1, // 23: routerrpc.PaymentStatus.state:type_name -> routerrpc.PaymentState - 51, // 24: routerrpc.PaymentStatus.htlcs:type_name -> lnrpc.HTLCAttempt - 39, // 25: routerrpc.ForwardHtlcInterceptRequest.incoming_circuit_key:type_name -> routerrpc.CircuitKey - 45, // 26: routerrpc.ForwardHtlcInterceptRequest.custom_records:type_name -> routerrpc.ForwardHtlcInterceptRequest.CustomRecordsEntry - 39, // 27: routerrpc.ForwardHtlcInterceptResponse.incoming_circuit_key:type_name -> routerrpc.CircuitKey - 2, // 28: routerrpc.ForwardHtlcInterceptResponse.action:type_name -> routerrpc.ResolveHoldForwardAction - 50, // 29: routerrpc.ForwardHtlcInterceptResponse.failure_code:type_name -> lnrpc.Failure.FailureCode - 52, // 30: routerrpc.UpdateChanStatusRequest.chan_point:type_name -> lnrpc.ChannelPoint - 3, // 31: routerrpc.UpdateChanStatusRequest.action:type_name -> routerrpc.ChanStatusAction - 5, // 32: routerrpc.Router.SendPaymentV2:input_type -> routerrpc.SendPaymentRequest - 6, // 33: routerrpc.Router.TrackPaymentV2:input_type -> routerrpc.TrackPaymentRequest - 7, // 34: routerrpc.Router.TrackPayments:input_type -> routerrpc.TrackPaymentsRequest - 8, // 35: routerrpc.Router.EstimateRouteFee:input_type -> routerrpc.RouteFeeRequest - 10, // 36: routerrpc.Router.SendToRoute:input_type -> routerrpc.SendToRouteRequest - 10, // 37: routerrpc.Router.SendToRouteV2:input_type -> routerrpc.SendToRouteRequest - 12, // 38: routerrpc.Router.ResetMissionControl:input_type -> routerrpc.ResetMissionControlRequest - 14, // 39: routerrpc.Router.QueryMissionControl:input_type -> routerrpc.QueryMissionControlRequest - 16, // 40: routerrpc.Router.XImportMissionControl:input_type -> routerrpc.XImportMissionControlRequest - 20, // 41: routerrpc.Router.GetMissionControlConfig:input_type -> routerrpc.GetMissionControlConfigRequest - 22, // 42: routerrpc.Router.SetMissionControlConfig:input_type -> routerrpc.SetMissionControlConfigRequest - 25, // 43: routerrpc.Router.QueryProbability:input_type -> routerrpc.QueryProbabilityRequest - 27, // 44: routerrpc.Router.BuildRoute:input_type -> routerrpc.BuildRouteRequest - 29, // 45: routerrpc.Router.SubscribeHtlcEvents:input_type -> routerrpc.SubscribeHtlcEventsRequest - 5, // 46: routerrpc.Router.SendPayment:input_type -> routerrpc.SendPaymentRequest - 6, // 47: routerrpc.Router.TrackPayment:input_type -> routerrpc.TrackPaymentRequest - 41, // 48: routerrpc.Router.HtlcInterceptor:input_type -> routerrpc.ForwardHtlcInterceptResponse - 42, // 49: routerrpc.Router.UpdateChanStatus:input_type -> routerrpc.UpdateChanStatusRequest - 53, // 50: routerrpc.Router.SendPaymentV2:output_type -> lnrpc.Payment - 53, // 51: routerrpc.Router.TrackPaymentV2:output_type -> lnrpc.Payment - 53, // 52: routerrpc.Router.TrackPayments:output_type -> lnrpc.Payment - 9, // 53: routerrpc.Router.EstimateRouteFee:output_type -> routerrpc.RouteFeeResponse - 11, // 54: routerrpc.Router.SendToRoute:output_type -> routerrpc.SendToRouteResponse - 51, // 55: routerrpc.Router.SendToRouteV2:output_type -> lnrpc.HTLCAttempt - 13, // 56: routerrpc.Router.ResetMissionControl:output_type -> routerrpc.ResetMissionControlResponse - 15, // 57: routerrpc.Router.QueryMissionControl:output_type -> routerrpc.QueryMissionControlResponse - 17, // 58: routerrpc.Router.XImportMissionControl:output_type -> routerrpc.XImportMissionControlResponse - 21, // 59: routerrpc.Router.GetMissionControlConfig:output_type -> routerrpc.GetMissionControlConfigResponse - 23, // 60: routerrpc.Router.SetMissionControlConfig:output_type -> routerrpc.SetMissionControlConfigResponse - 26, // 61: routerrpc.Router.QueryProbability:output_type -> routerrpc.QueryProbabilityResponse - 28, // 62: routerrpc.Router.BuildRoute:output_type -> routerrpc.BuildRouteResponse - 30, // 63: routerrpc.Router.SubscribeHtlcEvents:output_type -> routerrpc.HtlcEvent - 38, // 64: routerrpc.Router.SendPayment:output_type -> routerrpc.PaymentStatus - 38, // 65: routerrpc.Router.TrackPayment:output_type -> routerrpc.PaymentStatus - 40, // 66: routerrpc.Router.HtlcInterceptor:output_type -> routerrpc.ForwardHtlcInterceptRequest - 43, // 67: routerrpc.Router.UpdateChanStatus:output_type -> routerrpc.UpdateChanStatusResponse - 50, // [50:68] is the sub-list for method output_type - 32, // [32:50] is the sub-list for method input_type - 32, // [32:32] is the sub-list for extension type_name - 32, // [32:32] is the sub-list for extension extendee - 0, // [0:32] is the sub-list for field type_name + 49, // 0: routerrpc.SendPaymentRequest.route_hints:type_name -> lnrpc.RouteHint + 47, // 1: routerrpc.SendPaymentRequest.dest_custom_records:type_name -> routerrpc.SendPaymentRequest.DestCustomRecordsEntry + 50, // 2: routerrpc.SendPaymentRequest.dest_features:type_name -> lnrpc.FeatureBit + 51, // 3: routerrpc.SendToRouteRequest.route:type_name -> lnrpc.Route + 52, // 4: routerrpc.SendToRouteResponse.failure:type_name -> lnrpc.Failure + 19, // 5: routerrpc.QueryMissionControlResponse.pairs:type_name -> routerrpc.PairHistory + 19, // 6: routerrpc.XImportMissionControlRequest.pairs:type_name -> routerrpc.PairHistory + 20, // 7: routerrpc.PairHistory.history:type_name -> routerrpc.PairData + 25, // 8: routerrpc.GetMissionControlConfigResponse.config:type_name -> routerrpc.MissionControlConfig + 25, // 9: routerrpc.SetMissionControlConfigRequest.config:type_name -> routerrpc.MissionControlConfig + 4, // 10: routerrpc.MissionControlConfig.Model:type_name -> routerrpc.MissionControlConfig.ProbabilityModel + 27, // 11: routerrpc.MissionControlConfig.apriori:type_name -> routerrpc.AprioriParameters + 26, // 12: routerrpc.MissionControlConfig.bimodal:type_name -> routerrpc.BimodalParameters + 20, // 13: routerrpc.QueryProbabilityResponse.history:type_name -> routerrpc.PairData + 51, // 14: routerrpc.BuildRouteResponse.route:type_name -> lnrpc.Route + 5, // 15: routerrpc.HtlcEvent.event_type:type_name -> routerrpc.HtlcEvent.EventType + 35, // 16: routerrpc.HtlcEvent.forward_event:type_name -> routerrpc.ForwardEvent + 36, // 17: routerrpc.HtlcEvent.forward_fail_event:type_name -> routerrpc.ForwardFailEvent + 37, // 18: routerrpc.HtlcEvent.settle_event:type_name -> routerrpc.SettleEvent + 40, // 19: routerrpc.HtlcEvent.link_fail_event:type_name -> routerrpc.LinkFailEvent + 39, // 20: routerrpc.HtlcEvent.subscribed_event:type_name -> routerrpc.SubscribedEvent + 38, // 21: routerrpc.HtlcEvent.final_htlc_event:type_name -> routerrpc.FinalHtlcEvent + 34, // 22: routerrpc.ForwardEvent.info:type_name -> routerrpc.HtlcInfo + 34, // 23: routerrpc.LinkFailEvent.info:type_name -> routerrpc.HtlcInfo + 53, // 24: routerrpc.LinkFailEvent.wire_failure:type_name -> lnrpc.Failure.FailureCode + 0, // 25: routerrpc.LinkFailEvent.failure_detail:type_name -> routerrpc.FailureDetail + 1, // 26: routerrpc.PaymentStatus.state:type_name -> routerrpc.PaymentState + 54, // 27: routerrpc.PaymentStatus.htlcs:type_name -> lnrpc.HTLCAttempt + 42, // 28: routerrpc.ForwardHtlcInterceptRequest.incoming_circuit_key:type_name -> routerrpc.CircuitKey + 48, // 29: routerrpc.ForwardHtlcInterceptRequest.custom_records:type_name -> routerrpc.ForwardHtlcInterceptRequest.CustomRecordsEntry + 42, // 30: routerrpc.ForwardHtlcInterceptResponse.incoming_circuit_key:type_name -> routerrpc.CircuitKey + 2, // 31: routerrpc.ForwardHtlcInterceptResponse.action:type_name -> routerrpc.ResolveHoldForwardAction + 53, // 32: routerrpc.ForwardHtlcInterceptResponse.failure_code:type_name -> lnrpc.Failure.FailureCode + 55, // 33: routerrpc.UpdateChanStatusRequest.chan_point:type_name -> lnrpc.ChannelPoint + 3, // 34: routerrpc.UpdateChanStatusRequest.action:type_name -> routerrpc.ChanStatusAction + 6, // 35: routerrpc.Router.SendPaymentV2:input_type -> routerrpc.SendPaymentRequest + 7, // 36: routerrpc.Router.TrackPaymentV2:input_type -> routerrpc.TrackPaymentRequest + 8, // 37: routerrpc.Router.TrackPayments:input_type -> routerrpc.TrackPaymentsRequest + 9, // 38: routerrpc.Router.EstimateRouteFee:input_type -> routerrpc.RouteFeeRequest + 11, // 39: routerrpc.Router.SendToRoute:input_type -> routerrpc.SendToRouteRequest + 11, // 40: routerrpc.Router.SendToRouteV2:input_type -> routerrpc.SendToRouteRequest + 13, // 41: routerrpc.Router.ResetMissionControl:input_type -> routerrpc.ResetMissionControlRequest + 15, // 42: routerrpc.Router.QueryMissionControl:input_type -> routerrpc.QueryMissionControlRequest + 17, // 43: routerrpc.Router.XImportMissionControl:input_type -> routerrpc.XImportMissionControlRequest + 21, // 44: routerrpc.Router.GetMissionControlConfig:input_type -> routerrpc.GetMissionControlConfigRequest + 23, // 45: routerrpc.Router.SetMissionControlConfig:input_type -> routerrpc.SetMissionControlConfigRequest + 28, // 46: routerrpc.Router.QueryProbability:input_type -> routerrpc.QueryProbabilityRequest + 30, // 47: routerrpc.Router.BuildRoute:input_type -> routerrpc.BuildRouteRequest + 32, // 48: routerrpc.Router.SubscribeHtlcEvents:input_type -> routerrpc.SubscribeHtlcEventsRequest + 6, // 49: routerrpc.Router.SendPayment:input_type -> routerrpc.SendPaymentRequest + 7, // 50: routerrpc.Router.TrackPayment:input_type -> routerrpc.TrackPaymentRequest + 44, // 51: routerrpc.Router.HtlcInterceptor:input_type -> routerrpc.ForwardHtlcInterceptResponse + 45, // 52: routerrpc.Router.UpdateChanStatus:input_type -> routerrpc.UpdateChanStatusRequest + 56, // 53: routerrpc.Router.SendPaymentV2:output_type -> lnrpc.Payment + 56, // 54: routerrpc.Router.TrackPaymentV2:output_type -> lnrpc.Payment + 56, // 55: routerrpc.Router.TrackPayments:output_type -> lnrpc.Payment + 10, // 56: routerrpc.Router.EstimateRouteFee:output_type -> routerrpc.RouteFeeResponse + 12, // 57: routerrpc.Router.SendToRoute:output_type -> routerrpc.SendToRouteResponse + 54, // 58: routerrpc.Router.SendToRouteV2:output_type -> lnrpc.HTLCAttempt + 14, // 59: routerrpc.Router.ResetMissionControl:output_type -> routerrpc.ResetMissionControlResponse + 16, // 60: routerrpc.Router.QueryMissionControl:output_type -> routerrpc.QueryMissionControlResponse + 18, // 61: routerrpc.Router.XImportMissionControl:output_type -> routerrpc.XImportMissionControlResponse + 22, // 62: routerrpc.Router.GetMissionControlConfig:output_type -> routerrpc.GetMissionControlConfigResponse + 24, // 63: routerrpc.Router.SetMissionControlConfig:output_type -> routerrpc.SetMissionControlConfigResponse + 29, // 64: routerrpc.Router.QueryProbability:output_type -> routerrpc.QueryProbabilityResponse + 31, // 65: routerrpc.Router.BuildRoute:output_type -> routerrpc.BuildRouteResponse + 33, // 66: routerrpc.Router.SubscribeHtlcEvents:output_type -> routerrpc.HtlcEvent + 41, // 67: routerrpc.Router.SendPayment:output_type -> routerrpc.PaymentStatus + 41, // 68: routerrpc.Router.TrackPayment:output_type -> routerrpc.PaymentStatus + 43, // 69: routerrpc.Router.HtlcInterceptor:output_type -> routerrpc.ForwardHtlcInterceptRequest + 46, // 70: routerrpc.Router.UpdateChanStatus:output_type -> routerrpc.UpdateChanStatusResponse + 53, // [53:71] is the sub-list for method output_type + 35, // [35:53] is the sub-list for method input_type + 35, // [35:35] is the sub-list for extension type_name + 35, // [35:35] is the sub-list for extension extendee + 0, // [0:35] is the sub-list for field type_name } func init() { file_routerrpc_router_proto_init() } @@ -3941,7 +4243,7 @@ func file_routerrpc_router_proto_init() { } } file_routerrpc_router_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*QueryProbabilityRequest); i { + switch v := v.(*BimodalParameters); i { case 0: return &v.state case 1: @@ -3953,7 +4255,7 @@ func file_routerrpc_router_proto_init() { } } file_routerrpc_router_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*QueryProbabilityResponse); i { + switch v := v.(*AprioriParameters); i { case 0: return &v.state case 1: @@ -3965,7 +4267,7 @@ func file_routerrpc_router_proto_init() { } } file_routerrpc_router_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*BuildRouteRequest); i { + switch v := v.(*QueryProbabilityRequest); i { case 0: return &v.state case 1: @@ -3977,7 +4279,7 @@ func file_routerrpc_router_proto_init() { } } file_routerrpc_router_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*BuildRouteResponse); i { + switch v := v.(*QueryProbabilityResponse); i { case 0: return &v.state case 1: @@ -3989,7 +4291,7 @@ func file_routerrpc_router_proto_init() { } } file_routerrpc_router_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SubscribeHtlcEventsRequest); i { + switch v := v.(*BuildRouteRequest); i { case 0: return &v.state case 1: @@ -4001,7 +4303,7 @@ func file_routerrpc_router_proto_init() { } } file_routerrpc_router_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*HtlcEvent); i { + switch v := v.(*BuildRouteResponse); i { case 0: return &v.state case 1: @@ -4013,7 +4315,7 @@ func file_routerrpc_router_proto_init() { } } file_routerrpc_router_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*HtlcInfo); i { + switch v := v.(*SubscribeHtlcEventsRequest); i { case 0: return &v.state case 1: @@ -4025,7 +4327,7 @@ func file_routerrpc_router_proto_init() { } } file_routerrpc_router_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ForwardEvent); i { + switch v := v.(*HtlcEvent); i { case 0: return &v.state case 1: @@ -4037,7 +4339,7 @@ func file_routerrpc_router_proto_init() { } } file_routerrpc_router_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ForwardFailEvent); i { + switch v := v.(*HtlcInfo); i { case 0: return &v.state case 1: @@ -4049,7 +4351,7 @@ func file_routerrpc_router_proto_init() { } } file_routerrpc_router_proto_msgTypes[29].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SettleEvent); i { + switch v := v.(*ForwardEvent); i { case 0: return &v.state case 1: @@ -4061,7 +4363,7 @@ func file_routerrpc_router_proto_init() { } } file_routerrpc_router_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*FinalHtlcEvent); i { + switch v := v.(*ForwardFailEvent); i { case 0: return &v.state case 1: @@ -4073,7 +4375,7 @@ func file_routerrpc_router_proto_init() { } } file_routerrpc_router_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SubscribedEvent); i { + switch v := v.(*SettleEvent); i { case 0: return &v.state case 1: @@ -4085,7 +4387,7 @@ func file_routerrpc_router_proto_init() { } } file_routerrpc_router_proto_msgTypes[32].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*LinkFailEvent); i { + switch v := v.(*FinalHtlcEvent); i { case 0: return &v.state case 1: @@ -4097,7 +4399,7 @@ func file_routerrpc_router_proto_init() { } } file_routerrpc_router_proto_msgTypes[33].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PaymentStatus); i { + switch v := v.(*SubscribedEvent); i { case 0: return &v.state case 1: @@ -4109,7 +4411,7 @@ func file_routerrpc_router_proto_init() { } } file_routerrpc_router_proto_msgTypes[34].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CircuitKey); i { + switch v := v.(*LinkFailEvent); i { case 0: return &v.state case 1: @@ -4121,7 +4423,7 @@ func file_routerrpc_router_proto_init() { } } file_routerrpc_router_proto_msgTypes[35].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ForwardHtlcInterceptRequest); i { + switch v := v.(*PaymentStatus); i { case 0: return &v.state case 1: @@ -4133,7 +4435,7 @@ func file_routerrpc_router_proto_init() { } } file_routerrpc_router_proto_msgTypes[36].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ForwardHtlcInterceptResponse); i { + switch v := v.(*CircuitKey); i { case 0: return &v.state case 1: @@ -4145,7 +4447,7 @@ func file_routerrpc_router_proto_init() { } } file_routerrpc_router_proto_msgTypes[37].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpdateChanStatusRequest); i { + switch v := v.(*ForwardHtlcInterceptRequest); i { case 0: return &v.state case 1: @@ -4157,6 +4459,30 @@ func file_routerrpc_router_proto_init() { } } file_routerrpc_router_proto_msgTypes[38].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ForwardHtlcInterceptResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_routerrpc_router_proto_msgTypes[39].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UpdateChanStatusRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_routerrpc_router_proto_msgTypes[40].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*UpdateChanStatusResponse); i { case 0: return &v.state @@ -4169,7 +4495,11 @@ func file_routerrpc_router_proto_init() { } } } - file_routerrpc_router_proto_msgTypes[25].OneofWrappers = []interface{}{ + file_routerrpc_router_proto_msgTypes[19].OneofWrappers = []interface{}{ + (*MissionControlConfig_Apriori)(nil), + (*MissionControlConfig_Bimodal)(nil), + } + file_routerrpc_router_proto_msgTypes[27].OneofWrappers = []interface{}{ (*HtlcEvent_ForwardEvent)(nil), (*HtlcEvent_ForwardFailEvent)(nil), (*HtlcEvent_SettleEvent)(nil), @@ -4182,8 +4512,8 @@ func file_routerrpc_router_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_routerrpc_router_proto_rawDesc, - NumEnums: 5, - NumMessages: 41, + NumEnums: 6, + NumMessages: 43, NumExtensions: 0, NumServices: 1, }, diff --git a/lnrpc/routerrpc/router.proto b/lnrpc/routerrpc/router.proto index 600511fe4..4b8ae441f 100644 --- a/lnrpc/routerrpc/router.proto +++ b/lnrpc/routerrpc/router.proto @@ -467,6 +467,93 @@ message SetMissionControlConfigResponse { } message MissionControlConfig { + /* + Deprecated, use AprioriParameters. The amount of time mission control will + take to restore a penalized node or channel back to 50% success probability, + expressed in seconds. Setting this value to a higher value will penalize + failures for longer, making mission control less likely to route through + nodes and channels that we have previously recorded failures for. + */ + uint64 half_life_seconds = 1 [deprecated = true]; + + /* + Deprecated, use AprioriParameters. The probability of success mission + control should assign to hop in a route where it has no other information + available. Higher values will make mission control more willing to try hops + that we have no information about, lower values will discourage trying these + hops. + */ + float hop_probability = 2 [deprecated = true]; + + /* + Deprecated, use AprioriParameters. The importance that mission control + should place on historical results, expressed as a value in [0;1]. Setting + this value to 1 will ignore all historical payments and just use the hop + probability to assess the probability of success for each hop. A zero value + ignores hop probability completely and relies entirely on historical + results, unless none are available. + */ + float weight = 3 [deprecated = true]; + + /* + The maximum number of payment results that mission control will store. + */ + uint32 maximum_payment_results = 4; + + /* + The minimum time that must have passed since the previously recorded failure + before we raise the failure amount. + */ + uint64 minimum_failure_relax_interval = 5; + + enum ProbabilityModel { + APRIORI = 0; + BIMODAL = 1; + } + + /* + ProbabilityModel defines which probability estimator should be used in + pathfinding. + */ + ProbabilityModel Model = 6; + + /* + EstimatorConfig is populated dependent on the estimator type. + */ + oneof EstimatorConfig { + AprioriParameters apriori = 7; + BimodalParameters bimodal = 8; + } +} + +message BimodalParameters { + /* + NodeWeight defines how strongly other previous forwardings on channels of a + router should be taken into account when computing a channel's probability + to route. The allowed values are in the range [0, 1], where a value of 0 + means that only direct information about a channel is taken into account. + */ + double node_weight = 1; + + /* + ScaleMsat describes the scale over which channels statistically have some + liquidity left. The value determines how quickly the bimodal distribution + drops off from the edges of a channel. A larger value (compared to typical + channel capacities) means that the drop off is slow and that channel + balances are distributed more uniformly. A small value leads to the + assumption of very unbalanced channels. + */ + uint64 scale_msat = 2; + + /* + DecayTime describes the information decay of knowledge about previous + successes and failures in channels. The smaller the decay time, the quicker + we forget about past forwardings. + */ + uint64 decay_time = 3; +} + +message AprioriParameters { /* The amount of time mission control will take to restore a penalized node or channel back to 50% success probability, expressed in seconds. Setting @@ -482,7 +569,7 @@ message MissionControlConfig { control more willing to try hops that we have no information about, lower values will discourage trying these hops. */ - float hop_probability = 2; + double hop_probability = 2; /* The importance that mission control should place on historical results, @@ -492,18 +579,7 @@ message MissionControlConfig { completely and relies entirely on historical results, unless none are available. */ - float weight = 3; - - /* - The maximum number of payment results that mission control will store. - */ - uint32 maximum_payment_results = 4; - - /* - The minimum time that must have passed since the previously recorded failure - before we raise the failure amount. - */ - uint64 minimum_failure_relax_interval = 5; + double weight = 3; } message QueryProbabilityRequest { diff --git a/lnrpc/routerrpc/router.swagger.json b/lnrpc/routerrpc/router.swagger.json index e428c57e6..a841ba73c 100644 --- a/lnrpc/routerrpc/router.swagger.json +++ b/lnrpc/routerrpc/router.swagger.json @@ -593,6 +593,14 @@ ], "default": "IN_FLIGHT" }, + "MissionControlConfigProbabilityModel": { + "type": "string", + "enum": [ + "APRIORI", + "BIMODAL" + ], + "default": "APRIORI" + }, "lnrpcAMPRecord": { "type": "object", "properties": { @@ -1073,6 +1081,46 @@ } } }, + "routerrpcAprioriParameters": { + "type": "object", + "properties": { + "half_life_seconds": { + "type": "string", + "format": "uint64", + "description": "The amount of time mission control will take to restore a penalized node\nor channel back to 50% success probability, expressed in seconds. Setting\nthis value to a higher value will penalize failures for longer, making\nmission control less likely to route through nodes and channels that we\nhave previously recorded failures for." + }, + "hop_probability": { + "type": "number", + "format": "double", + "description": "The probability of success mission control should assign to hop in a route\nwhere it has no other information available. Higher values will make mission\ncontrol more willing to try hops that we have no information about, lower\nvalues will discourage trying these hops." + }, + "weight": { + "type": "number", + "format": "double", + "description": "The importance that mission control should place on historical results,\nexpressed as a value in [0;1]. Setting this value to 1 will ignore all\nhistorical payments and just use the hop probability to assess the\nprobability of success for each hop. A zero value ignores hop probability\ncompletely and relies entirely on historical results, unless none are\navailable." + } + } + }, + "routerrpcBimodalParameters": { + "type": "object", + "properties": { + "node_weight": { + "type": "number", + "format": "double", + "description": "NodeWeight defines how strongly other previous forwardings on channels of a\nrouter should be taken into account when computing a channel's probability\nto route. The allowed values are in the range [0, 1], where a value of 0\nmeans that only direct information about a channel is taken into account." + }, + "scale_msat": { + "type": "string", + "format": "uint64", + "description": "ScaleMsat describes the scale over which channels statistically have some\nliquidity left. The value determines how quickly the bimodal distribution\ndrops off from the edges of a channel. A larger value (compared to typical\nchannel capacities) means that the drop off is slow and that channel\nbalances are distributed more uniformly. A small value leads to the\nassumption of very unbalanced channels." + }, + "decay_time": { + "type": "string", + "format": "uint64", + "description": "DecayTime describes the information decay of knowledge about previous\nsuccesses and failures in channels. The smaller the decay time, the quicker\nwe forget about past forwardings." + } + } + }, "routerrpcBuildRouteRequest": { "type": "object", "properties": { @@ -1400,17 +1448,17 @@ "half_life_seconds": { "type": "string", "format": "uint64", - "description": "The amount of time mission control will take to restore a penalized node\nor channel back to 50% success probability, expressed in seconds. Setting\nthis value to a higher value will penalize failures for longer, making\nmission control less likely to route through nodes and channels that we\nhave previously recorded failures for." + "description": "Deprecated, use AprioriParameters. The amount of time mission control will\ntake to restore a penalized node or channel back to 50% success probability,\nexpressed in seconds. Setting this value to a higher value will penalize\nfailures for longer, making mission control less likely to route through\nnodes and channels that we have previously recorded failures for." }, "hop_probability": { "type": "number", "format": "float", - "description": "The probability of success mission control should assign to hop in a route\nwhere it has no other information available. Higher values will make mission\ncontrol more willing to try hops that we have no information about, lower\nvalues will discourage trying these hops." + "description": "Deprecated, use AprioriParameters. The probability of success mission\ncontrol should assign to hop in a route where it has no other information\navailable. Higher values will make mission control more willing to try hops\nthat we have no information about, lower values will discourage trying these\nhops." }, "weight": { "type": "number", "format": "float", - "description": "The importance that mission control should place on historical results,\nexpressed as a value in [0;1]. Setting this value to 1 will ignore all\nhistorical payments and just use the hop probability to assess the\nprobability of success for each hop. A zero value ignores hop probability\ncompletely and relies entirely on historical results, unless none are\navailable." + "description": "Deprecated, use AprioriParameters. The importance that mission control\nshould place on historical results, expressed as a value in [0;1]. Setting\nthis value to 1 will ignore all historical payments and just use the hop\nprobability to assess the probability of success for each hop. A zero value\nignores hop probability completely and relies entirely on historical\nresults, unless none are available." }, "maximum_payment_results": { "type": "integer", @@ -1421,6 +1469,16 @@ "type": "string", "format": "uint64", "description": "The minimum time that must have passed since the previously recorded failure\nbefore we raise the failure amount." + }, + "Model": { + "$ref": "#/definitions/MissionControlConfigProbabilityModel", + "description": "ProbabilityModel defines which probability estimator should be used in\npathfinding." + }, + "apriori": { + "$ref": "#/definitions/routerrpcAprioriParameters" + }, + "bimodal": { + "$ref": "#/definitions/routerrpcBimodalParameters" } } }, diff --git a/lnrpc/routerrpc/router_server.go b/lnrpc/routerrpc/router_server.go index 44335b84b..a11a63d6c 100644 --- a/lnrpc/routerrpc/router_server.go +++ b/lnrpc/routerrpc/router_server.go @@ -454,39 +454,137 @@ func (s *Server) GetMissionControlConfig(ctx context.Context, req *GetMissionControlConfigRequest) (*GetMissionControlConfigResponse, error) { + // Query the current mission control config. cfg := s.cfg.RouterBackend.MissionControl.GetConfig() - return &GetMissionControlConfigResponse{ + resp := &GetMissionControlConfigResponse{ Config: &MissionControlConfig{ - HalfLifeSeconds: uint64(cfg.PenaltyHalfLife.Seconds()), - HopProbability: float32(cfg.AprioriHopProbability), - Weight: float32(cfg.AprioriWeight), - MaximumPaymentResults: uint32(cfg.MaxMcHistory), - MinimumFailureRelaxInterval: uint64(cfg.MinFailureRelaxInterval.Seconds()), + MaximumPaymentResults: uint32(cfg.MaxMcHistory), + MinimumFailureRelaxInterval: uint64( + cfg.MinFailureRelaxInterval.Seconds(), + ), }, - }, nil + } + + // We only populate fields based on the current estimator. + switch v := cfg.Estimator.Config().(type) { + case routing.AprioriConfig: + resp.Config.Model = MissionControlConfig_APRIORI + aCfg := AprioriParameters{ + HalfLifeSeconds: uint64(v.PenaltyHalfLife.Seconds()), + HopProbability: v.AprioriHopProbability, + Weight: v.AprioriWeight, + } + + // Populate deprecated fields. + resp.Config.HalfLifeSeconds = uint64( + v.PenaltyHalfLife.Seconds(), + ) + resp.Config.HopProbability = float32(v.AprioriHopProbability) + resp.Config.Weight = float32(v.AprioriWeight) + + resp.Config.EstimatorConfig = &MissionControlConfig_Apriori{ + Apriori: &aCfg, + } + + case routing.BimodalConfig: + resp.Config.Model = MissionControlConfig_BIMODAL + bCfg := BimodalParameters{ + NodeWeight: v.BimodalNodeWeight, + ScaleMsat: uint64(v.BimodalScaleMsat), + DecayTime: uint64(v.BimodalDecayTime.Seconds()), + } + + resp.Config.EstimatorConfig = &MissionControlConfig_Bimodal{ + Bimodal: &bCfg, + } + + default: + return nil, fmt.Errorf("unknown estimator config type %T", v) + } + + return resp, nil } -// SetMissionControlConfig returns our current mission control config. +// SetMissionControlConfig sets parameters in the mission control config. func (s *Server) SetMissionControlConfig(ctx context.Context, req *SetMissionControlConfigRequest) (*SetMissionControlConfigResponse, error) { - cfg := &routing.MissionControlConfig{ - ProbabilityEstimatorCfg: routing.ProbabilityEstimatorCfg{ - PenaltyHalfLife: time.Duration( - req.Config.HalfLifeSeconds, - ) * time.Second, - AprioriHopProbability: float64(req.Config.HopProbability), - AprioriWeight: float64(req.Config.Weight), - }, + mcCfg := &routing.MissionControlConfig{ MaxMcHistory: int(req.Config.MaximumPaymentResults), MinFailureRelaxInterval: time.Duration( req.Config.MinimumFailureRelaxInterval, ) * time.Second, } + switch req.Config.Model { + case MissionControlConfig_APRIORI: + var aprioriConfig routing.AprioriConfig + + // Determine the apriori config with backward compatibility + // should the api use deprecated fields. + switch v := req.Config.EstimatorConfig.(type) { + case *MissionControlConfig_Bimodal: + return nil, fmt.Errorf("bimodal config " + + "provided, but apriori model requested") + + case *MissionControlConfig_Apriori: + aprioriConfig = routing.AprioriConfig{ + PenaltyHalfLife: time.Duration( + v.Apriori.HalfLifeSeconds, + ) * time.Second, + AprioriHopProbability: v.Apriori.HopProbability, + AprioriWeight: v.Apriori.Weight, + } + + default: + aprioriConfig = routing.AprioriConfig{ + PenaltyHalfLife: time.Duration( + int64(req.Config.HalfLifeSeconds), + ) * time.Second, + AprioriHopProbability: float64( + req.Config.HopProbability, + ), + AprioriWeight: float64(req.Config.Weight), + } + } + + estimator, err := routing.NewAprioriEstimator(aprioriConfig) + if err != nil { + return nil, err + } + mcCfg.Estimator = estimator + + case MissionControlConfig_BIMODAL: + cfg, ok := req.Config. + EstimatorConfig.(*MissionControlConfig_Bimodal) + if !ok { + return nil, fmt.Errorf("bimodal estimator requested " + + "but corresponding config not set") + } + bCfg := cfg.Bimodal + + bimodalConfig := routing.BimodalConfig{ + BimodalDecayTime: time.Duration( + bCfg.DecayTime, + ) * time.Second, + BimodalScaleMsat: lnwire.MilliSatoshi(bCfg.ScaleMsat), + BimodalNodeWeight: bCfg.NodeWeight, + } + + estimator, err := routing.NewBimodalEstimator(bimodalConfig) + if err != nil { + return nil, err + } + mcCfg.Estimator = estimator + + default: + return nil, fmt.Errorf("unknown estimator type %v", + req.Config.Model) + } + return &SetMissionControlConfigResponse{}, - s.cfg.RouterBackend.MissionControl.SetConfig(cfg) + s.cfg.RouterBackend.MissionControl.SetConfig(mcCfg) } // QueryMissionControl exposes the internal mission control state to callers. It diff --git a/lnrpc/routerrpc/routing_config.go b/lnrpc/routerrpc/routing_config.go index 2a53ba2e4..a1bbad590 100644 --- a/lnrpc/routerrpc/routing_config.go +++ b/lnrpc/routerrpc/routing_config.go @@ -7,28 +7,16 @@ import ( ) // RoutingConfig contains the configurable parameters that control routing. +// +//nolint:lll type RoutingConfig struct { + // ProbabilityEstimatorType sets the estimator to use. + ProbabilityEstimatorType string `long:"estimator" choice:"apriori" choice:"bimodal" description:"Probability estimator used for pathfinding." ` + // MinRouteProbability 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."` - - // AprioriWeight is a value in the range [0, 1] that defines to what - // extent historical results should be extrapolated to untried - // connections. Setting it to one will completely ignore historical - // results and always assume the configured a priori probability for - // untried connections. A value of zero will ignore the a priori - // probability completely and only base the probability on historical - // results, unless there are none available. - AprioriWeight float64 `long:"aprioriweight" description:"Weight of the a priori probability in success probability estimation. Valid values are in [0, 1]."` - - // 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 fixed virtual cost in path finding of a failed // payment attempt. It is used to trade off potentially better routes // against their probability of succeeding. @@ -47,4 +35,51 @@ type RoutingConfig struct { // McFlushInterval defines the timer interval to use to flush mission // control state to the DB. McFlushInterval time.Duration `long:"mcflushinterval" description:"the timer interval to use to flush mission control state to the DB"` + + // AprioriConfig defines parameters for the apriori probability. + AprioriConfig *AprioriConfig `group:"apriori" namespace:"apriori" description:"configuration for the apriori pathfinding probability estimator"` + + // BimodalConfig defines parameters for the bimodal probability. + BimodalConfig *BimodalConfig `group:"bimodal" namespace:"bimodal" description:"configuration for the bimodal pathfinding probability estimator"` +} + +// AprioriConfig defines parameters for the apriori probability. +// +//nolint:lll +type AprioriConfig struct { + // HopProbability is the assumed success probability of a hop in a route + // when no other information is available. + HopProbability float64 `long:"hopprob" description:"Assumed success probability of a hop in a route when no other information is available."` + + // Weight is a value in the range [0, 1] that defines to what extent + // historical results should be extrapolated to untried connections. + // Setting it to one will completely ignore historical results and + // always assume the configured a priori probability for untried + // connections. A value of zero will ignore the a priori probability + // completely and only base the probability on historical results, + // unless there are none available. + Weight float64 `long:"weight" description:"Weight of the a priori probability in success probability estimation. Valid values are in [0, 1]."` + + // 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"` +} + +// BimodalConfig defines parameters for the bimodal probability. +// +//nolint:lll +type BimodalConfig struct { + // Scale describes the scale over which channels still have some + // liquidity left on both channel ends. A value of 0 means that we + // assume perfectly unbalanced channels, a very high value means + // randomly balanced channels. + Scale int64 `long:"scale" description:"Defines the unbalancedness assumed for the network, the amount defined in msat."` + + // NodeWeight defines how strongly non-routed channels should be taken + // into account for probability estimation. Valid values are in [0,1]. + NodeWeight float64 `long:"nodeweight" description:"Defines how strongly non-routed channels should be taken into account for probability estimation. Valid values are in [0, 1]."` + + // DecayTime is the scale for the exponential information decay over + // time for previous successes or failures. + DecayTime time.Duration `long:"decaytime" description:"Describes the information decay of knowledge about previous successes and failures in channels."` } diff --git a/lntemp/rpc/router.go b/lntemp/rpc/router.go index a61f7b8dc..fbf44cb18 100644 --- a/lntemp/rpc/router.go +++ b/lntemp/rpc/router.go @@ -86,6 +86,20 @@ func (h *HarnessRPC) SetMissionControlConfig( h.NoError(err, "SetMissionControlConfig") } +// SetMissionControlConfigAssertErr makes a RPC call to the node's +// SetMissionControlConfig and asserts that we error. +func (h *HarnessRPC) SetMissionControlConfigAssertErr( + config *routerrpc.MissionControlConfig) { + + ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout) + defer cancel() + + req := &routerrpc.SetMissionControlConfigRequest{Config: config} + _, err := h.Router.SetMissionControlConfig(ctxt, req) + require.Error(h, err, "expect an error from setting import mission "+ + "control") +} + // ResetMissionControl makes a RPC call to the node's ResetMissionControl and // asserts. func (h *HarnessRPC) ResetMissionControl() { diff --git a/lntest/itest/lnd_routing_test.go b/lntest/itest/lnd_routing_test.go index b94e3b488..53d9f2b97 100644 --- a/lntest/itest/lnd_routing_test.go +++ b/lntest/itest/lnd_routing_test.go @@ -1003,25 +1003,85 @@ func testQueryRoutes(ht *lntemp.HarnessTest) { func testMissionControlCfg(t *testing.T, hn *node.HarnessNode) { t.Helper() - startCfg := hn.RPC.GetMissionControlConfig() + // Getting and setting does not alter the configuration. + startCfg := hn.RPC.GetMissionControlConfig().Config + hn.RPC.SetMissionControlConfig(startCfg) + resp := hn.RPC.GetMissionControlConfig() + require.True(t, proto.Equal(startCfg, resp.Config)) + // We test that setting and getting leads to the same config if all + // fields are set. cfg := &routerrpc.MissionControlConfig{ - HalfLifeSeconds: 8000, - HopProbability: 0.8, - Weight: 0.3, MaximumPaymentResults: 30, MinimumFailureRelaxInterval: 60, + Model: routerrpc. + MissionControlConfig_APRIORI, + EstimatorConfig: &routerrpc.MissionControlConfig_Apriori{ + Apriori: &routerrpc.AprioriParameters{ + HalfLifeSeconds: 8000, + HopProbability: 0.8, + Weight: 0.3, + }, + }, } - hn.RPC.SetMissionControlConfig(cfg) - resp := hn.RPC.GetMissionControlConfig() - require.True(t, proto.Equal(cfg, resp.Config)) + // The deprecated fields should be populated. + cfg.HalfLifeSeconds = 8000 + cfg.HopProbability = 0.8 + cfg.Weight = 0.3 + respCfg := hn.RPC.GetMissionControlConfig().Config + require.True(t, proto.Equal(cfg, respCfg)) - hn.RPC.SetMissionControlConfig(startCfg.Config) + // Switching to another estimator is possible. + cfg = &routerrpc.MissionControlConfig{ + Model: routerrpc. + MissionControlConfig_BIMODAL, + EstimatorConfig: &routerrpc.MissionControlConfig_Bimodal{ + Bimodal: &routerrpc.BimodalParameters{ + ScaleMsat: 1_000, + DecayTime: 500, + }, + }, + } + hn.RPC.SetMissionControlConfig(cfg) + respCfg = hn.RPC.GetMissionControlConfig().Config + require.NotNil(t, respCfg.GetBimodal()) + // If parameters are not set in the request, they will have zero values + // after. + require.Zero(t, respCfg.MaximumPaymentResults) + require.Zero(t, respCfg.MinimumFailureRelaxInterval) + require.Zero(t, respCfg.GetBimodal().NodeWeight) + + // Setting deprecated values will initialize the apriori estimator. + cfg = &routerrpc.MissionControlConfig{ + MaximumPaymentResults: 30, + MinimumFailureRelaxInterval: 60, + HopProbability: 0.8, + Weight: 0.3, + HalfLifeSeconds: 8000, + } + hn.RPC.SetMissionControlConfig(cfg) + respCfg = hn.RPC.GetMissionControlConfig().Config + require.NotNil(t, respCfg.GetApriori()) + + // Setting the wrong config results in an error. + cfg = &routerrpc.MissionControlConfig{ + Model: routerrpc. + MissionControlConfig_APRIORI, + EstimatorConfig: &routerrpc.MissionControlConfig_Bimodal{ + Bimodal: &routerrpc.BimodalParameters{ + ScaleMsat: 1_000, + }, + }, + } + hn.RPC.SetMissionControlConfigAssertErr(cfg) + + // Undo any changes. + hn.RPC.SetMissionControlConfig(startCfg) resp = hn.RPC.GetMissionControlConfig() - require.True(t, proto.Equal(startCfg.Config, resp.Config)) + require.True(t, proto.Equal(startCfg, resp.Config)) } // testMissionControlImport tests import of mission control results from an diff --git a/routing/integrated_routing_context_test.go b/routing/integrated_routing_context_test.go index 3c5b63c6f..0ab0c1b01 100644 --- a/routing/integrated_routing_context_test.go +++ b/routing/integrated_routing_context_test.go @@ -11,6 +11,7 @@ import ( "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/routing/route" "github.com/lightningnetwork/lnd/zpay32" + "github.com/stretchr/testify/require" ) const ( @@ -68,6 +69,14 @@ func newIntegratedRoutingContext(t *testing.T) *integratedRoutingContext { // defaults would break the unit tests. The actual values picked aren't // critical to excite certain behavior, but do need to be aligned with // the test case assertions. + aCfg := AprioriConfig{ + PenaltyHalfLife: 30 * time.Minute, + AprioriHopProbability: 0.6, + AprioriWeight: 0.5, + } + estimator, err := NewAprioriEstimator(aCfg) + require.NoError(t, err) + ctx := integratedRoutingContext{ t: t, graph: graph, @@ -75,11 +84,7 @@ func newIntegratedRoutingContext(t *testing.T) *integratedRoutingContext { finalExpiry: 40, mcCfg: MissionControlConfig{ - ProbabilityEstimatorCfg: ProbabilityEstimatorCfg{ - PenaltyHalfLife: 30 * time.Minute, - AprioriHopProbability: 0.6, - AprioriWeight: 0.5, - }, + Estimator: estimator, }, pathFindingCfg: PathFindingConfig{ diff --git a/routing/integrated_routing_test.go b/routing/integrated_routing_test.go index 2dd2cf632..f4c99a9a1 100644 --- a/routing/integrated_routing_test.go +++ b/routing/integrated_routing_test.go @@ -67,7 +67,10 @@ func TestProbabilityExtrapolation(t *testing.T) { // If we use a static value for the node probability (no extrapolation // of data from other channels), all ten bad channels will be tried // first before switching to the paid channel. - ctx.mcCfg.AprioriWeight = 1 + estimator, ok := ctx.mcCfg.Estimator.(*AprioriEstimator) + if ok { + estimator.AprioriWeight = 1 + } attempts, err = ctx.testPayment(1) require.NoError(t, err, "payment failed") if len(attempts) != 11 { diff --git a/routing/missioncontrol.go b/routing/missioncontrol.go index 86de4bc87..201777af3 100644 --- a/routing/missioncontrol.go +++ b/routing/missioncontrol.go @@ -103,7 +103,7 @@ type MissionControl struct { // estimator is the probability estimator that is used with the payment // results that mission control collects. - estimator *probabilityEstimator + estimator Estimator sync.Mutex @@ -116,9 +116,8 @@ type MissionControl struct { // MissionControlConfig defines parameters that control mission control // behaviour. type MissionControlConfig struct { - // ProbabilityEstimatorConfig is the config we will use for probability - // calculations. - ProbabilityEstimatorCfg + // Estimator gives probability estimates for node pairs. + Estimator Estimator // MaxMcHistory defines the maximum number of payment results that are // held on disk. @@ -135,10 +134,6 @@ type MissionControlConfig struct { } func (c *MissionControlConfig) validate() error { - if err := c.ProbabilityEstimatorCfg.validate(); err != nil { - return err - } - if c.MaxMcHistory < 0 { return ErrInvalidMcHistory } @@ -152,11 +147,8 @@ func (c *MissionControlConfig) validate() error { // String returns a string representation of a mission control config. func (c *MissionControlConfig) String() string { - return fmt.Sprintf("Penalty Half Life: %v, Apriori Hop "+ - "Probablity: %v, Maximum History: %v, Apriori Weight: %v, "+ - "Minimum Failure Relax Interval: %v", c.PenaltyHalfLife, - c.AprioriHopProbability, c.MaxMcHistory, c.AprioriWeight, - c.MinFailureRelaxInterval) + return fmt.Sprintf("maximum history: %v, minimum failure relax "+ + "interval: %v", c.MaxMcHistory, c.MinFailureRelaxInterval) } // TimedPairResult describes a timestamped pair result. @@ -212,7 +204,8 @@ type paymentResult struct { func NewMissionControl(db kvdb.Backend, self route.Vertex, cfg *MissionControlConfig) (*MissionControl, error) { - log.Debugf("Instantiating mission control with config: %v", cfg) + log.Debugf("Instantiating mission control with config: %v, %v", cfg, + cfg.Estimator) if err := cfg.validate(); err != nil { return nil, err @@ -225,17 +218,12 @@ func NewMissionControl(db kvdb.Backend, self route.Vertex, return nil, err } - estimator := &probabilityEstimator{ - ProbabilityEstimatorCfg: cfg.ProbabilityEstimatorCfg, - prevSuccessProbability: prevSuccessProbability, - } - mc := &MissionControl{ state: newMissionControlState(cfg.MinFailureRelaxInterval), now: time.Now, selfNode: self, store: store, - estimator: estimator, + estimator: cfg.Estimator, } if err := mc.init(); err != nil { @@ -284,7 +272,7 @@ func (m *MissionControl) GetConfig() *MissionControlConfig { defer m.Unlock() return &MissionControlConfig{ - ProbabilityEstimatorCfg: m.estimator.ProbabilityEstimatorCfg, + Estimator: m.estimator, MaxMcHistory: m.store.maxRecords, McFlushInterval: m.store.flushInterval, MinFailureRelaxInterval: m.state.minFailureRelaxInterval, @@ -305,11 +293,12 @@ func (m *MissionControl) SetConfig(cfg *MissionControlConfig) error { m.Lock() defer m.Unlock() - log.Infof("Updating mission control cfg: %v", cfg) + log.Infof("Active mission control cfg: %v, estimator: %v", cfg, + cfg.Estimator) m.store.maxRecords = cfg.MaxMcHistory m.state.minFailureRelaxInterval = cfg.MinFailureRelaxInterval - m.estimator.ProbabilityEstimatorCfg = cfg.ProbabilityEstimatorCfg + m.estimator = cfg.Estimator return nil } @@ -344,10 +333,10 @@ func (m *MissionControl) GetProbability(fromNode, toNode route.Vertex, // Use a distinct probability estimation function for local channels. if fromNode == m.selfNode { - return m.estimator.getLocalPairProbability(now, results, toNode) + return m.estimator.LocalPairProbability(now, results, toNode) } - return m.estimator.getPairProbability( + return m.estimator.PairProbability( now, results, toNode, amt, capacity, ) } diff --git a/routing/missioncontrol_test.go b/routing/missioncontrol_test.go index 6bbe4aaf5..91cc8eaa0 100644 --- a/routing/missioncontrol_test.go +++ b/routing/missioncontrol_test.go @@ -90,15 +90,17 @@ func (ctx *mcTestContext) restartMc() { require.NoError(ctx.t, ctx.mc.store.storeResults()) } + aCfg := AprioriConfig{ + PenaltyHalfLife: testPenaltyHalfLife, + AprioriHopProbability: testAprioriHopProbability, + AprioriWeight: testAprioriWeight, + } + estimator, err := NewAprioriEstimator(aCfg) + require.NoError(ctx.t, err) + mc, err := NewMissionControl( ctx.db, mcTestSelf, - &MissionControlConfig{ - ProbabilityEstimatorCfg: ProbabilityEstimatorCfg{ - PenaltyHalfLife: testPenaltyHalfLife, - AprioriHopProbability: testAprioriHopProbability, - AprioriWeight: testAprioriWeight, - }, - }, + &MissionControlConfig{Estimator: estimator}, ) if err != nil { ctx.t.Fatal(err) diff --git a/routing/pathfind.go b/routing/pathfind.go index bffc46336..f204591b6 100644 --- a/routing/pathfind.go +++ b/routing/pathfind.go @@ -46,6 +46,10 @@ type pathFinder = func(g *graphParams, r *RestrictParams, []*channeldb.CachedEdgePolicy, float64, error) var ( + // DefaultEstimator is the default estimator used for computing + // probabilities in pathfinding. + DefaultEstimator = AprioriEstimatorName + // DefaultAttemptCost is the default fixed virtual cost in path finding // of a failed payment attempt. It is used to trade off potentially // better routes against their probability of succeeding. diff --git a/routing/probability_apriori.go b/routing/probability_apriori.go new file mode 100644 index 000000000..dabd47ff9 --- /dev/null +++ b/routing/probability_apriori.go @@ -0,0 +1,346 @@ +package routing + +import ( + "errors" + "fmt" + "math" + "time" + + "github.com/btcsuite/btcd/btcutil" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing/route" +) + +const ( + // capacityCutoffFraction and capacitySmearingFraction define how + // capacity-related probability reweighting works. + // capacityCutoffFraction defines the fraction of the channel capacity + // at which the effect roughly sets in and capacitySmearingFraction + // defines over which range the factor changes from 1 to 0. + // + // We may fall below the minimum required probability + // (DefaultMinRouteProbability) when the amount comes close to the + // available capacity of a single channel of the route in case of no + // prior knowledge about the channels. We want such routes still to be + // available and therefore a probability reduction should not completely + // drop the total probability below DefaultMinRouteProbability. + // For this to hold for a three-hop route we require: + // (DefaultAprioriHopProbability)^3 * minCapacityFactor > + // DefaultMinRouteProbability + // + // For DefaultAprioriHopProbability = 0.6 and + // DefaultMinRouteProbability = 0.01 this results in + // minCapacityFactor ~ 0.05. The following combination of parameters + // fulfill the requirement with capacityFactor(cap, cap) ~ 0.076 (see + // tests). + + // The capacityCutoffFraction is a trade-off between usage of the + // provided capacity and expected probability reduction when we send the + // full amount. The success probability in the random balance model can + // be approximated with P(a) = 1 - a/c, for amount a and capacity c. If + // we require a probability P(a) > 0.25, this translates into a value of + // 0.75 for a/c. + capacityCutoffFraction = 0.75 + + // We don't want to have a sharp drop of the capacity factor to zero at + // capacityCutoffFraction, but a smooth smearing such that some residual + // probability is left when spending the whole amount, see above. + capacitySmearingFraction = 0.1 + + // AprioriEstimatorName is used to identify the apriori probability + // estimator. + AprioriEstimatorName = "apriori" +) + +var ( + // ErrInvalidHalflife is returned when we get an invalid half life. + ErrInvalidHalflife = errors.New("penalty half life must be >= 0") + + // ErrInvalidHopProbability is returned when we get an invalid hop + // probability. + ErrInvalidHopProbability = errors.New("hop probability must be in " + + "[0, 1]") + + // ErrInvalidAprioriWeight is returned when we get an apriori weight + // that is out of range. + ErrInvalidAprioriWeight = errors.New("apriori weight must be in [0, 1]") +) + +// AprioriConfig contains configuration for our probability estimator. +type AprioriConfig struct { + // PenaltyHalfLife defines after how much time a penalized node or + // channel is back at 50% probability. + PenaltyHalfLife time.Duration + + // AprioriHopProbability is the assumed success probability of a hop in + // a route when no other information is available. + AprioriHopProbability float64 + + // AprioriWeight is a value in the range [0, 1] that defines to what + // extent historical results should be extrapolated to untried + // connections. Setting it to one will completely ignore historical + // results and always assume the configured a priori probability for + // untried connections. A value of zero will ignore the a priori + // probability completely and only base the probability on historical + // results, unless there are none available. + AprioriWeight float64 +} + +// validate checks the configuration of the estimator for allowed values. +func (p AprioriConfig) validate() error { + if p.PenaltyHalfLife < 0 { + return ErrInvalidHalflife + } + + if p.AprioriHopProbability < 0 || p.AprioriHopProbability > 1 { + return ErrInvalidHopProbability + } + + if p.AprioriWeight < 0 || p.AprioriWeight > 1 { + return ErrInvalidAprioriWeight + } + + return nil +} + +// DefaultAprioriConfig returns the default configuration for the estimator. +func DefaultAprioriConfig() AprioriConfig { + return AprioriConfig{ + PenaltyHalfLife: DefaultPenaltyHalfLife, + AprioriHopProbability: DefaultAprioriHopProbability, + AprioriWeight: DefaultAprioriWeight, + } +} + +// AprioriEstimator returns node and pair probabilities based on historical +// payment results. It uses a preconfigured success probability value for +// untried hops (AprioriHopProbability) and returns a high success probability +// for hops that could previously conduct a payment (prevSuccessProbability). +// Successful edges are retried until proven otherwise. Recently failed hops are +// penalized by an exponential time decay (PenaltyHalfLife), after which they +// are reconsidered for routing. If information was learned about a forwarding +// node, the information is taken into account to estimate a per node +// probability that mixes with the a priori probability (AprioriWeight). +type AprioriEstimator struct { + // AprioriConfig contains configuration options for our estimator. + AprioriConfig + + // prevSuccessProbability is the assumed probability for node pairs that + // successfully relayed the previous attempt. + prevSuccessProbability float64 +} + +// NewAprioriEstimator creates a new AprioriEstimator. +func NewAprioriEstimator(cfg AprioriConfig) (*AprioriEstimator, error) { + if err := cfg.validate(); err != nil { + return nil, err + } + + return &AprioriEstimator{ + AprioriConfig: cfg, + prevSuccessProbability: prevSuccessProbability, + }, nil +} + +// Compile-time checks that interfaces are implemented. +var _ Estimator = (*AprioriEstimator)(nil) +var _ estimatorConfig = (*AprioriConfig)(nil) + +// Config returns the estimator's configuration. +func (p *AprioriEstimator) Config() estimatorConfig { + return p.AprioriConfig +} + +// String returns the estimator's configuration as a string representation. +func (p *AprioriEstimator) String() string { + return fmt.Sprintf("estimator type: %v, penalty halflife time: %v, "+ + "apriori hop probability: %v, apriori weight: %v, previous "+ + "success probability: %v", AprioriEstimatorName, + p.PenaltyHalfLife, p.AprioriHopProbability, p.AprioriWeight, + p.prevSuccessProbability) +} + +// getNodeProbability calculates the probability for connections from a node +// that have not been tried before. The results parameter is a list of last +// payment results for that node. +func (p *AprioriEstimator) getNodeProbability(now time.Time, + results NodeResults, amt lnwire.MilliSatoshi, + capacity btcutil.Amount) float64 { + + // We reduce the apriori hop probability if the amount comes close to + // the capacity. + apriori := p.AprioriHopProbability * capacityFactor(amt, capacity) + + // If the channel history is not to be taken into account, we can return + // early here with the configured a priori probability. + if p.AprioriWeight == 1 { + return apriori + } + + // If there is no channel history, our best estimate is still the a + // priori probability. + if len(results) == 0 { + return apriori + } + + // The value of the apriori weight is in the range [0, 1]. Convert it to + // a factor that properly expresses the intention of the weight in the + // following weight average calculation. When the apriori weight is 0, + // the apriori factor is also 0. This means it won't have any effect on + // the weighted average calculation below. When the apriori weight + // approaches 1, the apriori factor goes to infinity. It will heavily + // outweigh any observations that have been collected. + aprioriFactor := 1/(1-p.AprioriWeight) - 1 + + // Calculate a weighted average consisting of the apriori probability + // and historical observations. This is the part that incentivizes nodes + // to make sure that all (not just some) of their channels are in good + // shape. Senders will steer around nodes that have shown a few + // failures, even though there may be many channels still untried. + // + // If there is just a single observation and the apriori weight is 0, + // this single observation will totally determine the node probability. + // The node probability is returned for all other channels of the node. + // This means that one failure will lead to the success probability + // estimates for all other channels being 0 too. The probability for the + // channel that was tried will not even recover, because it is + // recovering to the node probability (which is zero). So one failure + // effectively prunes all channels of the node forever. This is the most + // aggressive way in which we can penalize nodes and unlikely to yield + // good results in a real network. + probabilitiesTotal := apriori * aprioriFactor + totalWeight := aprioriFactor + + for _, result := range results { + switch { + // Weigh success with a constant high weight of 1. There is no + // decay. Amt is never zero, so this clause is never executed + // when result.SuccessAmt is zero. + case amt <= result.SuccessAmt: + totalWeight++ + probabilitiesTotal += p.prevSuccessProbability + + // Weigh failures in accordance with their age. The base + // probability of a failure is considered zero, so nothing needs + // to be added to probabilitiesTotal. + case !result.FailTime.IsZero() && amt >= result.FailAmt: + age := now.Sub(result.FailTime) + totalWeight += p.getWeight(age) + } + } + + return probabilitiesTotal / totalWeight +} + +// getWeight calculates a weight in the range [0, 1] that should be assigned to +// a payment result. Weight follows an exponential curve that starts at 1 when +// the result is fresh and asymptotically approaches zero over time. The rate at +// which this happens is controlled by the penaltyHalfLife parameter. +func (p *AprioriEstimator) getWeight(age time.Duration) float64 { + exp := -age.Hours() / p.PenaltyHalfLife.Hours() + return math.Pow(2, exp) +} + +// capacityFactor is a multiplier that can be used to reduce the probability +// depending on how much of the capacity is sent. The limits are 1 for amt == 0 +// and 0 for amt >> cutoffMsat. The function drops significantly when amt +// reaches cutoffMsat. smearingMsat determines over which scale the reduction +// takes place. +func capacityFactor(amt lnwire.MilliSatoshi, capacity btcutil.Amount) float64 { + // If we don't have information about the capacity, which can be the + // case for hop hints or local channels, we return unity to not alter + // anything. + if capacity == 0 { + return 1.0 + } + + capMsat := float64(lnwire.NewMSatFromSatoshis(capacity)) + amtMsat := float64(amt) + + if amtMsat > capMsat { + return 0 + } + + cutoffMsat := capacityCutoffFraction * capMsat + smearingMsat := capacitySmearingFraction * capMsat + + // We compute a logistic function mirrored around the y axis, centered + // at cutoffMsat, decaying over the smearingMsat scale. + denominator := 1 + math.Exp(-(amtMsat-cutoffMsat)/smearingMsat) + + return 1 - 1/denominator +} + +// PairProbability estimates the probability of successfully traversing to +// toNode based on historical payment outcomes for the from node. Those outcomes +// are passed in via the results parameter. +func (p *AprioriEstimator) PairProbability(now time.Time, + results NodeResults, toNode route.Vertex, amt lnwire.MilliSatoshi, + capacity btcutil.Amount) float64 { + + nodeProbability := p.getNodeProbability(now, results, amt, capacity) + + return p.calculateProbability( + now, results, nodeProbability, toNode, amt, + ) +} + +// LocalPairProbability estimates the probability of successfully traversing +// our own local channels to toNode. +func (p *AprioriEstimator) LocalPairProbability( + now time.Time, results NodeResults, toNode route.Vertex) float64 { + + // For local channels that have never been tried before, we assume them + // to be successful. We have accurate balance and online status + // information on our own channels, so when we select them in a route it + // is close to certain that those channels will work. + nodeProbability := p.prevSuccessProbability + + return p.calculateProbability( + now, results, nodeProbability, toNode, lnwire.MaxMilliSatoshi, + ) +} + +// calculateProbability estimates the probability of successfully traversing to +// toNode based on historical payment outcomes and a fall-back node probability. +func (p *AprioriEstimator) calculateProbability( + now time.Time, results NodeResults, + nodeProbability float64, toNode route.Vertex, + amt lnwire.MilliSatoshi) float64 { + + // Retrieve the last pair outcome. + lastPairResult, ok := results[toNode] + + // If there is no history for this pair, return the node probability + // that is a probability estimate for untried channel. + if !ok { + return nodeProbability + } + + // For successes, we have a fixed (high) probability. Those pairs will + // be assumed good until proven otherwise. Amt is never zero, so this + // clause is never executed when lastPairResult.SuccessAmt is zero. + if amt <= lastPairResult.SuccessAmt { + return p.prevSuccessProbability + } + + // Take into account a minimum penalize amount. For balance errors, a + // failure may be reported with such a minimum to prevent too aggressive + // penalization. If the current amount is smaller than the amount that + // previously triggered a failure, we act as if this is an untried + // channel. + if lastPairResult.FailTime.IsZero() || amt < lastPairResult.FailAmt { + return nodeProbability + } + + timeSinceLastFailure := now.Sub(lastPairResult.FailTime) + + // Calculate success probability based on the weight of the last + // failure. When the failure is fresh, its weight is 1 and we'll return + // probability 0. Over time the probability recovers to the node + // probability. It would be as if this channel was never tried before. + weight := p.getWeight(timeSinceLastFailure) + probability := nodeProbability * (1 - weight) + + return probability +} diff --git a/routing/probability_apriori_test.go b/routing/probability_apriori_test.go new file mode 100644 index 000000000..a055d4ae2 --- /dev/null +++ b/routing/probability_apriori_test.go @@ -0,0 +1,289 @@ +package routing + +import ( + "testing" + "time" + + "github.com/btcsuite/btcd/btcutil" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing/route" + "github.com/stretchr/testify/require" +) + +const ( + // Define node identifiers. + node1 = 1 + node2 = 2 + node3 = 3 + + // untriedNode is a node id for which we don't record any results in + // this test. This can be used to assert the probability for untried + // ndoes. + untriedNode = 255 + + // Define test estimator parameters. + aprioriHopProb = 0.6 + aprioriWeight = 0.75 + aprioriPrevSucProb = 0.95 + + // testCapacity is used to define a capacity for some channels. + testCapacity = btcutil.Amount(100_000) + testAmount = lnwire.MilliSatoshi(50_000_000) + + // Defines the capacityFactor for testAmount and testCapacity. + capFactor = 0.9241 +) + +type estimatorTestContext struct { + t *testing.T + estimator *AprioriEstimator + + // results contains a list of last results. Every element in the list + // corresponds to the last result towards a node. The list index equals + // the node id. So the first element in the list is the result towards + // node 0. + results map[int]TimedPairResult +} + +func newEstimatorTestContext(t *testing.T) *estimatorTestContext { + return &estimatorTestContext{ + t: t, + estimator: &AprioriEstimator{ + AprioriConfig: AprioriConfig{ + AprioriHopProbability: aprioriHopProb, + AprioriWeight: aprioriWeight, + PenaltyHalfLife: time.Hour, + }, + prevSuccessProbability: aprioriPrevSucProb, + }, + } +} + +// assertPairProbability asserts that the calculated success probability is +// correct. +func (c *estimatorTestContext) assertPairProbability(now time.Time, + toNode byte, amt lnwire.MilliSatoshi, capacity btcutil.Amount, + expectedProb float64) { + + c.t.Helper() + + results := make(NodeResults) + for i, r := range c.results { + results[route.Vertex{byte(i)}] = r + } + + const tolerance = 0.01 + + p := c.estimator.PairProbability( + now, results, route.Vertex{toNode}, amt, capacity, + ) + diff := p - expectedProb + if diff > tolerance || diff < -tolerance { + c.t.Fatalf("expected probability %v for node %v, but got %v", + expectedProb, toNode, p) + } +} + +// TestProbabilityEstimatorNoResults tests the probability estimation when no +// results are available. +func TestProbabilityEstimatorNoResults(t *testing.T) { + t.Parallel() + + ctx := newEstimatorTestContext(t) + + // A zero amount does not trigger capacity rescaling. + ctx.assertPairProbability( + testTime, 0, 0, testCapacity, aprioriHopProb, + ) + + // We expect a reduced probability when a higher amount is used. + expected := aprioriHopProb * capFactor + ctx.assertPairProbability( + testTime, 0, testAmount, testCapacity, expected, + ) +} + +// TestProbabilityEstimatorOneSuccess tests the probability estimation for nodes +// that have a single success result. +func TestProbabilityEstimatorOneSuccess(t *testing.T) { + t.Parallel() + + ctx := newEstimatorTestContext(t) + + ctx.results = map[int]TimedPairResult{ + node1: { + SuccessAmt: testAmount, + }, + } + + // Because of the previous success, this channel keep reporting a high + // probability. + ctx.assertPairProbability( + testTime, node1, 100, testCapacity, aprioriPrevSucProb, + ) + + // The apriori success probability indicates that in the past we were + // able to send the full amount. We don't want to reduce this + // probability with the capacity factor, which is tested here. + ctx.assertPairProbability( + testTime, node1, testAmount, testCapacity, aprioriPrevSucProb, + ) + + // Untried channels are also influenced by the success. With a + // aprioriWeight of 0.75, the a priori probability is assigned weight 3. + expectedP := (3*aprioriHopProb + 1*aprioriPrevSucProb) / 4 + ctx.assertPairProbability( + testTime, untriedNode, 100, testCapacity, expectedP, + ) + + // Check that the correct probability is computed for larger amounts. + apriori := aprioriHopProb * capFactor + + expectedP = (3*apriori + 1*aprioriPrevSucProb) / 4 + ctx.assertPairProbability( + testTime, untriedNode, testAmount, testCapacity, expectedP, + ) +} + +// TestProbabilityEstimatorOneFailure tests the probability estimation for nodes +// that have a single failure. +func TestProbabilityEstimatorOneFailure(t *testing.T) { + t.Parallel() + + ctx := newEstimatorTestContext(t) + + ctx.results = map[int]TimedPairResult{ + node1: { + FailTime: testTime.Add(-time.Hour), + FailAmt: lnwire.MilliSatoshi(50), + }, + } + + // For an untried node, we expected the node probability. The weight for + // the failure after one hour is 0.5. This makes the node probability + // 0.51: + expectedNodeProb := (3*aprioriHopProb + 0.5*0) / 3.5 + ctx.assertPairProbability( + testTime, untriedNode, 100, testCapacity, expectedNodeProb, + ) + + // The pair probability decays back to the node probability. With the + // weight at 0.5, we expected a pair probability of 0.5 * 0.51 = 0.25. + ctx.assertPairProbability( + testTime, node1, 100, testCapacity, expectedNodeProb/2, + ) +} + +// TestProbabilityEstimatorMix tests the probability estimation for nodes for +// which a mix of successes and failures is recorded. +func TestProbabilityEstimatorMix(t *testing.T) { + t.Parallel() + + ctx := newEstimatorTestContext(t) + + ctx.results = map[int]TimedPairResult{ + node1: { + SuccessAmt: lnwire.MilliSatoshi(1000), + }, + node2: { + FailTime: testTime.Add(-2 * time.Hour), + FailAmt: lnwire.MilliSatoshi(50), + }, + node3: { + FailTime: testTime.Add(-3 * time.Hour), + FailAmt: lnwire.MilliSatoshi(50), + }, + } + + // We expect the probability for a previously successful channel to + // remain high. + ctx.assertPairProbability( + testTime, node1, 100, testCapacity, prevSuccessProbability, + ) + + // For an untried node, we expected the node probability to be returned. + // This is a weighted average of the results above and the a priori + // probability: 0.62. + expectedNodeProb := (3*aprioriHopProb + 1*prevSuccessProbability) / + (3 + 1 + 0.25 + 0.125) + + ctx.assertPairProbability( + testTime, untriedNode, 100, testCapacity, expectedNodeProb, + ) + + // For the previously failed connection with node 1, we expect 0.75 * + // the node probability = 0.47. + ctx.assertPairProbability( + testTime, node2, 100, testCapacity, expectedNodeProb*0.75, + ) +} + +// TestCapacityCutoff tests the mathematical expression and limits for the +// capacity factor. +func TestCapacityCutoff(t *testing.T) { + t.Parallel() + + capacitySat := 1_000_000 + capacityMSat := capacitySat * 1000 + + tests := []struct { + name string + amountMsat int + expectedFactor float64 + }{ + { + name: "zero amount", + expectedFactor: 1, + }, + { + name: "low amount", + amountMsat: capacityMSat / 10, + expectedFactor: 0.998, + }, + { + name: "half amount", + amountMsat: capacityMSat / 2, + expectedFactor: 0.924, + }, + { + name: "cutoff amount", + amountMsat: int( + capacityCutoffFraction * float64(capacityMSat), + ), + expectedFactor: 0.5, + }, + { + name: "high amount", + amountMsat: capacityMSat * 80 / 100, + expectedFactor: 0.377, + }, + { + // Even when we spend the full capacity, we still want + // to have some residual probability to not throw away + // routes due to a min probability requirement of the + // whole path. + name: "full amount", + amountMsat: capacityMSat, + expectedFactor: 0.076, + }, + { + name: "more than capacity", + amountMsat: capacityMSat + 1, + expectedFactor: 0.0, + }, + } + + for _, test := range tests { + test := test + + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + got := capacityFactor( + lnwire.MilliSatoshi(test.amountMsat), + btcutil.Amount(capacitySat), + ) + require.InDelta(t, test.expectedFactor, got, 0.001) + }) + } +} diff --git a/routing/probability_bimodal.go b/routing/probability_bimodal.go new file mode 100644 index 000000000..daa086fa9 --- /dev/null +++ b/routing/probability_bimodal.go @@ -0,0 +1,536 @@ +package routing + +import ( + "fmt" + "math" + "time" + + "github.com/btcsuite/btcd/btcutil" + "github.com/go-errors/errors" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing/route" +) + +const ( + // DefaultBimodalScaleMsat is the default value for BimodalScaleMsat in + // BimodalConfig. It describes the distribution of funds in the LN based + // on empirical findings. We assume an unbalanced network by default. + DefaultBimodalScaleMsat = lnwire.MilliSatoshi(300_000_000) + + // DefaultBimodalNodeWeight is the default value for the + // BimodalNodeWeight in BimodalConfig. It is chosen such that past + // forwardings on other channels of a router are only slightly taken + // into account. + DefaultBimodalNodeWeight = 0.2 + + // DefaultBimodalDecayTime is the default value for BimodalDecayTime. + // We will forget about previous learnings about channel liquidity on + // the timescale of about a week. + DefaultBimodalDecayTime = 7 * 24 * time.Hour + + // BimodalScaleMsatLimit is the maximum value for BimodalScaleMsat to + // avoid numerical issues. + BimodalScaleMsatMax = lnwire.MilliSatoshi(21e17) + + // BimodalEstimatorName is used to identify the bimodal estimator. + BimodalEstimatorName = "bimodal" +) + +var ( + // ErrInvalidScale is returned when we get a scale below or equal zero. + ErrInvalidScale = errors.New("scale must be >= 0 and sane") + + // ErrInvalidNodeWeight is returned when we get a node weight that is + // out of range. + ErrInvalidNodeWeight = errors.New("node weight must be in [0, 1]") + + // ErrInvalidDecayTime is returned when we get a decay time below zero. + ErrInvalidDecayTime = errors.New("decay time must be larger than zero") +) + +// BimodalConfig contains configuration for our probability estimator. +type BimodalConfig struct { + // BimodalNodeWeight defines how strongly other previous forwardings on + // channels of a router should be taken into account when computing a + // channel's probability to route. The allowed values are in the range + // [0, 1], where a value of 0 means that only direct information about a + // channel is taken into account. + BimodalNodeWeight float64 + + // BimodalScaleMsat describes the scale over which channels + // statistically have some liquidity left. The value determines how + // quickly the bimodal distribution drops off from the edges of a + // channel. A larger value (compared to typical channel capacities) + // means that the drop off is slow and that channel balances are + // distributed more uniformly. A small value leads to the assumption of + // very unbalanced channels. + BimodalScaleMsat lnwire.MilliSatoshi + + // BimodalDecayTime is the scale for the exponential information decay + // over time for previous successes or failures. + BimodalDecayTime time.Duration +} + +// validate checks the configuration of the estimator for allowed values. +func (p BimodalConfig) validate() error { + if p.BimodalDecayTime <= 0 { + return fmt.Errorf("%v: %w", BimodalEstimatorName, + ErrInvalidDecayTime) + } + + if p.BimodalNodeWeight < 0 || p.BimodalNodeWeight > 1 { + return fmt.Errorf("%v: %w", BimodalEstimatorName, + ErrInvalidNodeWeight) + } + + if p.BimodalScaleMsat == 0 || p.BimodalScaleMsat > BimodalScaleMsatMax { + return fmt.Errorf("%v: %w", BimodalEstimatorName, + ErrInvalidScale) + } + + return nil +} + +// DefaultBimodalConfig returns the default configuration for the estimator. +func DefaultBimodalConfig() BimodalConfig { + return BimodalConfig{ + BimodalNodeWeight: DefaultBimodalNodeWeight, + BimodalScaleMsat: DefaultBimodalScaleMsat, + BimodalDecayTime: DefaultBimodalDecayTime, + } +} + +// BimodalEstimator returns node and pair probabilities based on historical +// payment results based on a liquidity distribution model of the LN. The main +// function is to estimate the direct channel probability based on a depleted +// liquidity distribution model, with additional information decay over time. A +// per-node probability can be mixed with the direct probability, taking into +// account successes/failures on other channels of the forwarder. +type BimodalEstimator struct { + // BimodalConfig contains configuration options for our estimator. + BimodalConfig +} + +// NewBimodalEstimator creates a new BimodalEstimator. +func NewBimodalEstimator(cfg BimodalConfig) (*BimodalEstimator, error) { + if err := cfg.validate(); err != nil { + return nil, err + } + + return &BimodalEstimator{ + BimodalConfig: cfg, + }, nil +} + +// Compile-time checks that interfaces are implemented. +var _ Estimator = (*BimodalEstimator)(nil) +var _ estimatorConfig = (*BimodalConfig)(nil) + +// config returns the current configuration of the estimator. +func (p *BimodalEstimator) Config() estimatorConfig { + return p.BimodalConfig +} + +// String returns the estimator's configuration as a string representation. +func (p *BimodalEstimator) String() string { + return fmt.Sprintf("estimator type: %v, decay time: %v, liquidity "+ + "scale: %v, node weight: %v", BimodalEstimatorName, + p.BimodalDecayTime, p.BimodalScaleMsat, p.BimodalNodeWeight) +} + +// PairProbability estimates the probability of successfully traversing to +// toNode based on historical payment outcomes for the from node. Those outcomes +// are passed in via the results parameter. +func (p *BimodalEstimator) PairProbability(now time.Time, + results NodeResults, toNode route.Vertex, amt lnwire.MilliSatoshi, + capacity btcutil.Amount) float64 { + + // We first compute the probability for the desired hop taking into + // account previous knowledge. + directProbability := p.directProbability( + now, results, toNode, amt, lnwire.NewMSatFromSatoshis(capacity), + ) + + // The final probability is computed by taking into account other + // channels of the from node. + return p.calculateProbability(directProbability, now, results, toNode) +} + +// LocalPairProbability computes the probability to reach toNode given a set of +// previous learnings. +func (p *BimodalEstimator) LocalPairProbability(now time.Time, + results NodeResults, toNode route.Vertex) float64 { + + // For direct local probabilities we assume to know exactly how much we + // can send over a channel, which assumes that channels are active and + // have enough liquidity. + directProbability := 1.0 + + // If we had an unexpected failure for this node, we reduce the + // probability for some time to avoid infinite retries. + result, ok := results[toNode] + if ok && !result.FailTime.IsZero() { + timeAgo := now.Sub(result.FailTime) + + // We only expect results in the past to get a probability + // between 0 and 1. + if timeAgo < 0 { + timeAgo = 0 + } + exponent := -float64(timeAgo) / float64(p.BimodalDecayTime) + directProbability -= math.Exp(exponent) + } + + return directProbability +} + +// directProbability computes the probability to reach a node based on the +// liquidity distribution in the LN. +func (p *BimodalEstimator) directProbability(now time.Time, + results NodeResults, toNode route.Vertex, amt lnwire.MilliSatoshi, + capacity lnwire.MilliSatoshi) float64 { + + // We first determine the time-adjusted success and failure amounts to + // then compute a probability. We know that we can send a zero amount. + successAmount := lnwire.MilliSatoshi(0) + + // We know that we cannot send the full capacity. + failAmount := capacity + + // If we have information about past successes or failures, we modify + // them with a time decay. + result, ok := results[toNode] + if ok { + // Apply a time decay for the amount we cannot send. + if !result.FailTime.IsZero() { + failAmount = cannotSend( + result.FailAmt, capacity, now, result.FailTime, + p.BimodalDecayTime, + ) + } + + // Apply a time decay for the amount we can send. + if !result.SuccessTime.IsZero() { + successAmount = canSend( + result.SuccessAmt, now, result.SuccessTime, + p.BimodalDecayTime, + ) + } + } + + // Compute the direct channel probability. + probability, err := p.probabilityFormula( + capacity, successAmount, failAmount, amt, + ) + if err != nil { + log.Errorf("error computing probability: %v", err) + + return 0.0 + } + + return probability +} + +// calculateProbability computes the total hop probability combining the channel +// probability and historic forwarding data of other channels of the node we try +// to send from. +// +// Goals: +// * We want to incentivize good routing nodes: the more routable channels a +// node has, the more we want to incentivize (vice versa for failures). +// -> We reduce/increase the direct probability depending on past +// failures/successes for other channels of the node. +// +// * We want to be forgiving/give other nodes a chance as well: we want to +// forget about (non-)routable channels over time. +// -> We weight the successes/failures with a time decay such that they will not +// influence the total probability if a long time went by. +// +// * If we don't have other info, we want to solely rely on the direct +// probability. +// +// * We want to be able to specify how important the other channels are compared +// to the direct channel. +// -> Introduce a node weight factor that weights the direct probability against +// the node-wide average. The larger the node weight, the more important other +// channels of the node are. +// +// How do failures on low fee nodes redirect routing to higher fee nodes? +// Assumptions: +// * attemptCostPPM of 1000 PPM +// * constant direct channel probability of P0 (usually 0.5 for large amounts) +// * node weight w of 0.2 +// +// The question we want to answer is: +// How often would a zero-fee node be tried (even if there were failures for its +// other channels) over trying a high-fee node with 2000 PPM and no direct +// knowledge about the channel to send over? +// +// The probability of a route of length l is P(l) = l * P0. +// +// The total probability after n failures (with the implemented method here) is: +// P(l, n) = P(l-1) * P(n) +// = P(l-1) * (P0 + n*0) / (1 + n*w) +// = P(l) / (1 + n*w) +// +// Condition for a high-fee channel to overcome a low fee channel in the +// Dijkstra weight function (only looking at fee and probability PPM terms): +// highFeePPM + attemptCostPPM * 1/P(l) = 0PPM + attemptCostPPM * 1/P(l, n) +// highFeePPM/attemptCostPPM = 1/P(l, n) - 1/P(l) = +// = (1 + n*w)/P(l) - 1/P(l) = +// = n*w/P(l) +// +// Therefore: +// n = (highFeePPM/attemptCostPPM) * (P(l)/w) = +// = (2000/1000) * 0.5 * l / w = l/w +// +// For a one-hop route we get: +// n = 1/0.2 = 5 tolerated failures +// +// For a three-hop route we get: +// n = 3/0.2 = 15 tolerated failures +// +// For more details on the behavior see tests. +func (p *BimodalEstimator) calculateProbability(directProbability float64, + now time.Time, results NodeResults, toNode route.Vertex) float64 { + + // If we don't take other channels into account, we can return early. + if p.BimodalNodeWeight == 0.0 { + return directProbability + } + + // w is a parameter which determines how strongly the other channels of + // a node should be incorporated, the higher the stronger. + w := p.BimodalNodeWeight + + // dt determines the timeliness of the previous successes/failures + // to be taken into account. + dt := float64(p.BimodalDecayTime) + + // The direct channel probability is weighted fully, all other results + // are weighted according to how recent the information is. + totalProbabilities := directProbability + totalWeights := 1.0 + + for peer, result := range results { + // We don't include the direct hop probability here because it + // is already included in totalProbabilities. + if peer == toNode { + continue + } + + // We add probabilities weighted by how recent the info is. + var weight float64 + if result.SuccessAmt > 0 { + exponent := -float64(now.Sub(result.SuccessTime)) / dt + weight = math.Exp(exponent) + totalProbabilities += w * weight + totalWeights += w * weight + } + if result.FailAmt > 0 { + exponent := -float64(now.Sub(result.FailTime)) / dt + weight = math.Exp(exponent) + + // Failures don't add to total success probability. + totalWeights += w * weight + } + } + + return totalProbabilities / totalWeights +} + +// canSend returns the sendable amount over the channel, respecting time decay. +// canSend approaches zero, if we wait for a much longer time than the decay +// time. +func canSend(successAmount lnwire.MilliSatoshi, now, successTime time.Time, + decayConstant time.Duration) lnwire.MilliSatoshi { + + // The factor approaches 0 for successTime a long time in the past, + // is 1 when the successTime is now. + factor := math.Exp( + -float64(now.Sub(successTime)) / float64(decayConstant), + ) + + canSend := factor * float64(successAmount) + + return lnwire.MilliSatoshi(canSend) +} + +// cannotSend returns the not sendable amount over the channel, respecting time +// decay. cannotSend approaches the capacity, if we wait for a much longer time +// than the decay time. +func cannotSend(failAmount, capacity lnwire.MilliSatoshi, now, + failTime time.Time, decayConstant time.Duration) lnwire.MilliSatoshi { + + if failAmount > capacity { + failAmount = capacity + } + + // The factor approaches 0 for failTime a long time in the past and it + // is 1 when the failTime is now. + factor := math.Exp( + -float64(now.Sub(failTime)) / float64(decayConstant), + ) + + cannotSend := capacity - lnwire.MilliSatoshi( + factor*float64(capacity-failAmount), + ) + + return cannotSend +} + +// primitive computes the indefinite integral of our assumed (normalized) +// liquidity probability distribution. The distribution of liquidity x here is +// the function P(x) ~ exp(-x/s) + exp((x-c)/s), i.e., two exponentials residing +// at the ends of channels. This means that we expect liquidity to be at either +// side of the channel with capacity c. The s parameter (scale) defines how far +// the liquidity leaks into the channel. A very low scale assumes completely +// unbalanced channels, a very high scale assumes a random distribution. More +// details can be found in +// https://github.com/lightningnetwork/lnd/issues/5988#issuecomment-1131234858. +func (p *BimodalEstimator) primitive(c, x float64) float64 { + s := float64(p.BimodalScaleMsat) + + // The indefinite integral of P(x) is given by + // Int P(x) dx = H(x) = s * (-e(-x/s) + e((x-c)/s)), + // and its norm from 0 to c can be computed from it, + // norm = [H(x)]_0^c = s * (-e(-c/s) + 1 -(1 + e(-c/s))). + ecs := math.Exp(-c / s) + exs := math.Exp(-x / s) + + // It would be possible to split the next term and reuse the factors + // from before, but this can lead to numerical issues with large + // numbers. + excs := math.Exp((x - c) / s) + + // norm can only become zero, if c is zero, which we sorted out before + // calling this method. + norm := -2*ecs + 2 + + // We end up with the primitive function of the normalized P(x). + return (-exs + excs) / norm +} + +// integral computes the integral of our liquidity distribution from the lower +// to the upper value. +func (p *BimodalEstimator) integral(capacity, lower, upper float64) float64 { + if lower < 0 || lower > upper { + log.Errorf("probability integral limits nonsensical: capacity:"+ + "%v lower: %v upper: %v", capacity, lower, upper) + + return 0.0 + } + + return p.primitive(capacity, upper) - p.primitive(capacity, lower) +} + +// probabilityFormula computes the expected probability for a payment of +// amountMsat given prior learnings for a channel of certain capacity. +// successAmountMsat and failAmountMsat stand for the unsettled success and +// failure amounts, respectively. The formula is derived using the formalism +// presented in Pickhardt et al., https://arxiv.org/abs/2103.08576. +func (p *BimodalEstimator) probabilityFormula(capacityMsat, successAmountMsat, + failAmountMsat, amountMsat lnwire.MilliSatoshi) (float64, error) { + + // Convert to positive-valued floats. + capacity := float64(capacityMsat) + successAmount := float64(successAmountMsat) + failAmount := float64(failAmountMsat) + amount := float64(amountMsat) + + // Capacity being zero is a sentinel value to ignore the probability + // estimation, we'll return the full probability here. + if capacity == 0.0 { + return 1.0, nil + } + + // We cannot send more than the capacity. + if amount > capacity { + return 0.0, nil + } + + // Mission control may have some outdated values, we correct them here. + // TODO(bitromortac): there may be better decisions to make in these + // cases, e.g., resetting failAmount=cap and successAmount=0. + + // failAmount should be capacity at max. + if failAmount > capacity { + failAmount = capacity + } + + // successAmount should be capacity at max. + if successAmount > capacity { + successAmount = capacity + } + + // The next statement is a safety check against an illogical condition, + // otherwise the renormalization integral would become zero. This may + // happen if a large channel gets closed and smaller ones remain, but + // it should recover with the time decay. + if failAmount <= successAmount { + log.Tracef("fail amount (%v) is larger than or equal the "+ + "success amount (%v) for capacity (%v)", + failAmountMsat, successAmountMsat, capacityMsat) + + return 0.0, nil + } + + // We cannot send more than the fail amount. + if amount >= failAmount { + return 0.0, nil + } + + // The success probability for payment amount a is the integral over the + // prior distribution P(x), the probability to find liquidity between + // the amount a and channel capacity c (or failAmount a_f): + // P(X >= a | X < a_f) = Integral_{a}^{a_f} P(x) dx + prob := p.integral(capacity, amount, failAmount) + if math.IsNaN(prob) { + return 0.0, fmt.Errorf("non-normalized probability is NaN, "+ + "capacity: %v, amount: %v, fail amount: %v", + capacity, amount, failAmount) + } + + // If we have payment information, we need to adjust the prior + // distribution P(x) and get the posterior distribution by renormalizing + // the prior distribution in such a way that the probability mass lies + // between a_s and a_f. + reNorm := p.integral(capacity, successAmount, failAmount) + if math.IsNaN(reNorm) { + return 0.0, fmt.Errorf("normalization factor is NaN, "+ + "capacity: %v, success amount: %v, fail amount: %v", + capacity, successAmount, failAmount) + } + + // The normalization factor can only be zero if the success amount is + // equal or larger than the fail amount. This should not happen as we + // have checked this scenario above. + if reNorm == 0.0 { + return 0.0, fmt.Errorf("normalization factor is zero, "+ + "capacity: %v, success amount: %v, fail amount: %v", + capacity, successAmount, failAmount) + } + + prob /= reNorm + + // Note that for payment amounts smaller than successAmount, we can get + // a value larger than unity, which we cap here to get a proper + // probability. + if prob > 1.0 { + if amount > successAmount { + return 0.0, fmt.Errorf("unexpected large probability "+ + "(%v) capacity: %v, amount: %v, success "+ + "amount: %v, fail amount: %v", prob, capacity, + amount, successAmount, failAmount) + } + + return 1.0, nil + } else if prob < 0.0 { + return 0.0, fmt.Errorf("negative probability "+ + "(%v) capacity: %v, amount: %v, success "+ + "amount: %v, fail amount: %v", prob, capacity, + amount, successAmount, failAmount) + } + + return prob, nil +} diff --git a/routing/probability_bimodal_test.go b/routing/probability_bimodal_test.go new file mode 100644 index 000000000..53f8829c4 --- /dev/null +++ b/routing/probability_bimodal_test.go @@ -0,0 +1,664 @@ +package routing + +import ( + "math" + "testing" + "time" + + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing/route" + "github.com/stretchr/testify/require" +) + +const ( + smallAmount = lnwire.MilliSatoshi(400_000) + largeAmount = lnwire.MilliSatoshi(5_000_000) + capacity = lnwire.MilliSatoshi(10_000_000) + scale = lnwire.MilliSatoshi(400_000) +) + +// TestSuccessProbability tests that we get correct probability estimates for +// the direct channel probability. +func TestSuccessProbability(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + expectedProbability float64 + tolerance float64 + successAmount lnwire.MilliSatoshi + failAmount lnwire.MilliSatoshi + amount lnwire.MilliSatoshi + capacity lnwire.MilliSatoshi + }{ + // We can't send more than the capacity. + { + name: "no info, larger than capacity", + capacity: capacity, + successAmount: 0, + failAmount: capacity, + amount: capacity + 1, + expectedProbability: 0.0, + }, + // With the current model we don't prefer any channels if the + // send amount is large compared to the scale but small compared + // to the capacity. + { + name: "no info, large amount", + capacity: capacity, + successAmount: 0, + failAmount: capacity, + amount: largeAmount, + expectedProbability: 0.5, + }, + // We always expect to be able to "send" an amount of 0. + { + name: "no info, zero amount", + capacity: capacity, + successAmount: 0, + failAmount: capacity, + amount: 0, + expectedProbability: 1.0, + }, + // We can't send the whole capacity. + { + name: "no info, full capacity", + capacity: capacity, + successAmount: 0, + failAmount: capacity, + amount: capacity, + expectedProbability: 0.0, + }, + // Sending a small amount will have a higher probability to go + // through than a large amount. + { + name: "no info, small amount", + capacity: capacity, + successAmount: 0, + failAmount: capacity, + amount: smallAmount, + expectedProbability: 0.684, + tolerance: 0.001, + }, + // If we had an unsettled success, we are sure we can send a + // lower amount. + { + name: "previous success, lower amount", + capacity: capacity, + successAmount: largeAmount, + failAmount: capacity, + amount: smallAmount, + expectedProbability: 1.0, + }, + // If we had an unsettled success, we are sure we can send the + // same amount. + { + name: "previous success, success amount", + capacity: capacity, + successAmount: largeAmount, + failAmount: capacity, + amount: largeAmount, + expectedProbability: 1.0, + }, + // If we had an unsettled success with a small amount, we know + // with increased probability that we can send a comparable + // higher amount. + { + name: "previous success, larger amount", + capacity: capacity, + successAmount: smallAmount / 2, + failAmount: capacity, + amount: smallAmount, + expectedProbability: 0.851, + tolerance: 0.001, + }, + // If we had a large unsettled success before, we know we can + // send even larger payments with high probability. + { + name: "previous large success, larger " + + "amount", + capacity: capacity, + successAmount: largeAmount / 2, + failAmount: capacity, + amount: largeAmount, + expectedProbability: 0.998, + tolerance: 0.001, + }, + // If we had a failure before, we can't send with the fail + // amount. + { + name: "previous failure, fail amount", + capacity: capacity, + failAmount: largeAmount, + amount: largeAmount, + expectedProbability: 0.0, + }, + // We can't send a higher amount than the fail amount either. + { + name: "previous failure, larger fail " + + "amount", + capacity: capacity, + failAmount: largeAmount, + amount: largeAmount + smallAmount, + expectedProbability: 0.0, + }, + // We expect a diminished non-zero probability if we try to send + // an amount that's lower than the last fail amount. + { + name: "previous failure, lower than fail " + + "amount", + capacity: capacity, + failAmount: largeAmount, + amount: smallAmount, + expectedProbability: 0.368, + tolerance: 0.001, + }, + // From here on we deal with mixed previous successes and + // failures. + // We expect to be always able to send a tiny amount. + { + name: "previous f/s, very small amount", + capacity: capacity, + failAmount: largeAmount, + successAmount: smallAmount, + amount: 0, + expectedProbability: 1.0, + }, + // We expect to be able to send up to the previous success + // amount will full certainty. + { + name: "previous f/s, success amount", + capacity: capacity, + failAmount: largeAmount, + successAmount: smallAmount, + amount: smallAmount, + expectedProbability: 1.0, + }, + // This tests a random value between small amount and large + // amount. + { + name: "previous f/s, between f/s", + capacity: capacity, + failAmount: largeAmount, + successAmount: smallAmount, + amount: smallAmount + largeAmount/10, + expectedProbability: 0.287, + tolerance: 0.001, + }, + // We still can't send the fail amount. + { + name: "previous f/s, fail amount", + capacity: capacity, + failAmount: largeAmount, + successAmount: smallAmount, + amount: largeAmount, + expectedProbability: 0.0, + }, + // Same success and failure amounts (illogical). + { + name: "previous f/s, same", + capacity: capacity, + failAmount: largeAmount, + successAmount: largeAmount, + amount: largeAmount, + expectedProbability: 0.0, + }, + // Higher success than failure amount (illogical). + { + name: "previous f/s, higher success", + capacity: capacity, + failAmount: smallAmount, + successAmount: largeAmount, + expectedProbability: 0.0, + }, + } + + estimator := BimodalEstimator{ + BimodalConfig: BimodalConfig{BimodalScaleMsat: scale}, + } + + for _, test := range tests { + test := test + + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + p, err := estimator.probabilityFormula( + test.capacity, test.successAmount, + test.failAmount, test.amount, + ) + require.InDelta(t, test.expectedProbability, p, + test.tolerance) + require.NoError(t, err) + }) + } +} + +// TestIntegral tests certain limits of the probability distribution integral. +func TestIntegral(t *testing.T) { + t.Parallel() + + defaultScale := lnwire.NewMSatFromSatoshis(300_000) + + tests := []struct { + name string + capacity float64 + lower float64 + upper float64 + scale lnwire.MilliSatoshi + expected float64 + }{ + { + name: "all zero", + expected: math.NaN(), + scale: defaultScale, + }, + { + name: "all same", + capacity: 1, + lower: 1, + upper: 1, + scale: defaultScale, + }, + { + name: "large numbers, low lower", + capacity: 21e17, + lower: 0, + upper: 21e17, + expected: 1, + scale: defaultScale, + }, + { + name: "large numbers, high lower", + capacity: 21e17, + lower: 21e17, + upper: 21e17, + scale: defaultScale, + }, + { + name: "same scale and capacity", + capacity: 21e17, + lower: 21e17, + upper: 21e17, + scale: 21e17, + }, + } + + for _, test := range tests { + test := test + + t.Run(test.name, func(t *testing.T) { + t.Parallel() + estimator := BimodalEstimator{ + BimodalConfig: BimodalConfig{ + BimodalScaleMsat: test.scale, + }, + } + + p := estimator.integral( + test.capacity, test.lower, test.upper, + ) + require.InDelta(t, test.expected, p, 0.001) + }) + } +} + +// TestCanSend tests that the success amount drops to zero over time. +func TestCanSend(t *testing.T) { + t.Parallel() + + successAmount := lnwire.MilliSatoshi(1_000_000) + successTime := time.Unix(1_000, 0) + now := time.Unix(2_000, 0) + decayTime := time.Duration(1_000) * time.Second + infinity := time.Unix(10_000_000_000, 0) + + // Test an immediate retry. + require.Equal(t, successAmount, canSend( + successAmount, successTime, successTime, decayTime, + )) + + // Test that after the decay time, the successAmount is 1/e of its + // value. + decayAmount := lnwire.MilliSatoshi(float64(successAmount) / math.E) + require.Equal(t, decayAmount, canSend( + successAmount, now, successTime, decayTime, + )) + + // After a long time, we want the amount to approach 0. + require.Equal(t, lnwire.MilliSatoshi(0), canSend( + successAmount, infinity, successTime, decayTime, + )) +} + +// TestCannotSend tests that the fail amount approaches the capacity over time. +func TestCannotSend(t *testing.T) { + t.Parallel() + + failAmount := lnwire.MilliSatoshi(1_000_000) + failTime := time.Unix(1_000, 0) + now := time.Unix(2_000, 0) + decayTime := time.Duration(1_000) * time.Second + infinity := time.Unix(10_000_000_000, 0) + capacity := lnwire.MilliSatoshi(3_000_000) + + // Test immediate retry. + require.EqualValues(t, failAmount, cannotSend( + failAmount, capacity, failTime, failTime, decayTime, + )) + + // After the decay time we want to be between the fail amount and + // the capacity. + summand := lnwire.MilliSatoshi(float64(capacity-failAmount) / math.E) + expected := capacity - summand + require.Equal(t, expected, cannotSend( + failAmount, capacity, now, failTime, decayTime, + )) + + // After a long time, we want the amount to approach the capacity. + require.Equal(t, capacity, cannotSend( + failAmount, capacity, infinity, failTime, decayTime, + )) +} + +// TestComputeProbability tests the inclusion of previous forwarding results of +// other channels of the node into the total probability. +func TestComputeProbability(t *testing.T) { + t.Parallel() + + nodeWeight := 1 / 5. + toNode := route.Vertex{10} + tolerance := 0.01 + decayTime := time.Duration(1) * time.Hour * 24 + + // makeNodeResults prepares forwarding data for the other channels of + // the node. + makeNodeResults := func(successes []bool, now time.Time) NodeResults { + results := make(NodeResults, len(successes)) + + for i, s := range successes { + vertex := route.Vertex{byte(i)} + + results[vertex] = TimedPairResult{ + FailTime: now, FailAmt: 1, + } + if s { + results[vertex] = TimedPairResult{ + SuccessTime: now, SuccessAmt: 1, + } + } + } + + return results + } + + tests := []struct { + name string + directProbability float64 + otherResults []bool + expectedProbability float64 + delay time.Duration + }{ + // If no other information is available, use the direct + // probability. + { + name: "unknown, only direct", + directProbability: 0.5, + expectedProbability: 0.5, + }, + // If there was a single success, expect increased success + // probability. + { + name: "unknown, single success", + directProbability: 0.5, + otherResults: []bool{true}, + expectedProbability: 0.583, + }, + // If there were many successes, expect even higher success + // probability. + { + name: "unknown, many successes", + directProbability: 0.5, + otherResults: []bool{ + true, true, true, true, true, + }, + expectedProbability: 0.75, + }, + // If there was a single failure, we expect a slightly decreased + // probability. + { + name: "unknown, single failure", + directProbability: 0.5, + otherResults: []bool{false}, + expectedProbability: 0.416, + }, + // If there were many failures, we expect a strongly decreased + // probability. + { + name: "unknown, many failures", + directProbability: 0.5, + otherResults: []bool{ + false, false, false, false, false, + }, + expectedProbability: 0.25, + }, + // A success and a failure neutralize themselves. + { + name: "unknown, mixed even", + directProbability: 0.5, + otherResults: []bool{true, false}, + expectedProbability: 0.5, + }, + // A mixed result history leads to increase/decrease of the most + // experienced successes/failures. + { + name: "unknown, mixed uneven", + directProbability: 0.5, + otherResults: []bool{ + true, true, false, false, false, + }, + expectedProbability: 0.45, + }, + // Many successes don't elevate the probability above 1. + { + name: "success, successes", + directProbability: 1.0, + otherResults: []bool{ + true, true, true, true, true, + }, + expectedProbability: 1.0, + }, + // Five failures on a very certain channel will lower its + // success probability to the unknown probability. + { + name: "success, failures", + directProbability: 1.0, + otherResults: []bool{ + false, false, false, false, false, + }, + expectedProbability: 0.5, + }, + // If we are sure that the channel can send, a single failure + // will not decrease the outcome significantly. + { + name: "success, single failure", + directProbability: 1.0, + otherResults: []bool{false}, + expectedProbability: 0.8333, + }, + { + name: "success, many failures", + directProbability: 1.0, + otherResults: []bool{ + false, false, false, false, false, false, false, + }, + expectedProbability: 0.416, + }, + // Failures won't decrease the probability below zero. + { + name: "fail, failures", + directProbability: 0.0, + otherResults: []bool{false, false, false}, + expectedProbability: 0.0, + }, + { + name: "fail, successes", + directProbability: 0.0, + otherResults: []bool{ + true, true, true, true, true, + }, + expectedProbability: 0.5, + }, + // We test forgetting information with the time decay. + // A past success won't alter the certain success probability. + { + name: "success, single success, decay " + + "time", + directProbability: 1.0, + otherResults: []bool{true}, + delay: decayTime, + expectedProbability: 1.00, + }, + // A failure that was experienced some time ago won't influence + // as much as a recent one. + { + name: "success, single fail, decay time", + directProbability: 1.0, + otherResults: []bool{false}, + delay: decayTime, + expectedProbability: 0.9314, + }, + // Information from a long time ago doesn't have any effect. + { + name: "success, single fail, long ago", + directProbability: 1.0, + otherResults: []bool{false}, + delay: 10 * decayTime, + expectedProbability: 1.0, + }, + { + name: "fail, successes decay time", + directProbability: 0.0, + otherResults: []bool{ + true, true, true, true, true, + }, + delay: decayTime, + expectedProbability: 0.269, + }, + // Very recent info approaches the case with no time decay. + { + name: "unknown, successes close", + directProbability: 0.5, + otherResults: []bool{ + true, true, true, true, true, + }, + delay: decayTime / 10, + expectedProbability: 0.741, + }, + } + + estimator := BimodalEstimator{ + BimodalConfig: BimodalConfig{ + BimodalScaleMsat: scale, BimodalNodeWeight: nodeWeight, + BimodalDecayTime: decayTime, + }, + } + + for _, test := range tests { + test := test + + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + then := time.Unix(0, 0) + results := makeNodeResults(test.otherResults, then) + now := then.Add(test.delay) + + p := estimator.calculateProbability( + test.directProbability, now, results, toNode, + ) + + require.InDelta(t, test.expectedProbability, p, + tolerance) + }) + } +} + +// TestLocalPairProbability tests that we reduce probability for failed direct +// neighbors. +func TestLocalPairProbability(t *testing.T) { + t.Parallel() + + decayTime := time.Hour + now := time.Unix(1000000000, 0) + toNode := route.Vertex{1} + + createFailedResult := func(timeAgo time.Duration) NodeResults { + return NodeResults{ + toNode: TimedPairResult{ + FailTime: now.Add(-timeAgo), + }, + } + } + + tests := []struct { + name string + expectedProbability float64 + results NodeResults + }{ + { + name: "no results", + expectedProbability: 1.0, + }, + { + name: "recent failure", + results: createFailedResult(0), + expectedProbability: 0.0, + }, + { + name: "after decay time", + results: createFailedResult(decayTime), + expectedProbability: 1 - 1/math.E, + }, + { + name: "long ago", + results: createFailedResult(10 * decayTime), + expectedProbability: 1.0, + }, + } + + estimator := BimodalEstimator{ + BimodalConfig: BimodalConfig{BimodalDecayTime: decayTime}, + } + + for _, test := range tests { + test := test + + t.Run(test.name, func(t *testing.T) { + t.Parallel() + p := estimator.LocalPairProbability( + now, test.results, toNode, + ) + require.InDelta(t, test.expectedProbability, p, 0.001) + }) + } +} + +// FuzzProbability checks that we don't encounter errors related to NaNs. +func FuzzProbability(f *testing.F) { + estimator := BimodalEstimator{ + BimodalConfig: BimodalConfig{BimodalScaleMsat: scale}, + } + f.Add(uint64(0), uint64(0), uint64(0), uint64(0)) + + f.Fuzz(func(t *testing.T, capacity, successAmt, failAmt, amt uint64) { + _, err := estimator.probabilityFormula( + lnwire.MilliSatoshi(capacity), + lnwire.MilliSatoshi(successAmt), + lnwire.MilliSatoshi(failAmt), lnwire.MilliSatoshi(amt), + ) + + require.NoError(t, err, "c: %v s: %v f: %v a: %v", capacity, + successAmt, failAmt, amt) + }) +} diff --git a/routing/probability_estimator.go b/routing/probability_estimator.go index 320c16bda..c110b748d 100644 --- a/routing/probability_estimator.go +++ b/routing/probability_estimator.go @@ -1,8 +1,6 @@ package routing import ( - "errors" - "math" "time" "github.com/btcsuite/btcd/btcutil" @@ -10,285 +8,30 @@ import ( "github.com/lightningnetwork/lnd/routing/route" ) -const ( - // capacityCutoffFraction and capacitySmearingFraction define how - // capacity-related probability reweighting works. - // capacityCutoffFraction defines the fraction of the channel capacity - // at which the effect roughly sets in and capacitySmearingFraction - // defines over which range the factor changes from 1 to 0. - // - // We may fall below the minimum required probability - // (DefaultMinRouteProbability) when the amount comes close to the - // available capacity of a single channel of the route in case of no - // prior knowledge about the channels. We want such routes still to be - // available and therefore a probability reduction should not completely - // drop the total probability below DefaultMinRouteProbability. - // For this to hold for a three-hop route we require: - // (DefaultAprioriHopProbability)^3 * minCapacityFactor > - // DefaultMinRouteProbability - // - // For DefaultAprioriHopProbability = 0.6 and - // DefaultMinRouteProbability = 0.01 this results in - // minCapacityFactor ~ 0.05. The following combination of parameters - // fulfill the requirement with capacityFactor(cap, cap) ~ 0.076 (see - // tests). +// Estimator estimates the probability to reach a node. +type Estimator interface { + // PairProbability estimates the probability of successfully traversing + // to toNode based on historical payment outcomes for the from node. + // Those outcomes are passed in via the results parameter. + PairProbability(now time.Time, results NodeResults, + toNode route.Vertex, amt lnwire.MilliSatoshi, + capacity btcutil.Amount) float64 - // The capacityCutoffFraction is a trade-off between usage of the - // provided capacity and expected probability reduction when we send the - // full amount. The success probability in the random balance model can - // be approximated with P(a) = 1 - a/c, for amount a and capacity c. If - // we require a probability P(a) > 0.25, this translates into a value of - // 0.75 for a/c. - capacityCutoffFraction = 0.75 + // LocalPairProbability estimates the probability of successfully + // traversing our own local channels to toNode. + LocalPairProbability(now time.Time, results NodeResults, + toNode route.Vertex) float64 - // We don't want to have a sharp drop of the capacity factor to zero at - // capacityCutoffFraction, but a smooth smearing such that some residual - // probability is left when spending the whole amount, see above. - capacitySmearingFraction = 0.1 -) + // Config returns the estimator's configuration. + Config() estimatorConfig -var ( - // ErrInvalidHalflife is returned when we get an invalid half life. - ErrInvalidHalflife = errors.New("penalty half life must be >= 0") - - // ErrInvalidHopProbability is returned when we get an invalid hop - // probability. - ErrInvalidHopProbability = errors.New("hop probability must be in [0;1]") - - // ErrInvalidAprioriWeight is returned when we get an apriori weight - // that is out of range. - ErrInvalidAprioriWeight = errors.New("apriori weight must be in [0;1]") -) - -// ProbabilityEstimatorCfg contains configuration for our probability estimator. -type ProbabilityEstimatorCfg struct { - // PenaltyHalfLife defines after how much time a penalized node or - // channel is back at 50% probability. - PenaltyHalfLife time.Duration - - // AprioriHopProbability is the assumed success probability of a hop in - // a route when no other information is available. - AprioriHopProbability float64 - - // AprioriWeight is a value in the range [0, 1] that defines to what - // extent historical results should be extrapolated to untried - // connections. Setting it to one will completely ignore historical - // results and always assume the configured a priori probability for - // untried connections. A value of zero will ignore the a priori - // probability completely and only base the probability on historical - // results, unless there are none available. - AprioriWeight float64 + // String returns the string representation of the estimator's + // configuration. + String() string } -func (p ProbabilityEstimatorCfg) validate() error { - if p.PenaltyHalfLife < 0 { - return ErrInvalidHalflife - } - - if p.AprioriHopProbability < 0 || p.AprioriHopProbability > 1 { - return ErrInvalidHopProbability - } - - if p.AprioriWeight < 0 || p.AprioriWeight > 1 { - return ErrInvalidAprioriWeight - } - - return nil -} - -// probabilityEstimator returns node and pair probabilities based on historical -// payment results. -type probabilityEstimator struct { - // ProbabilityEstimatorCfg contains configuration options for our - // estimator. - ProbabilityEstimatorCfg - - // prevSuccessProbability is the assumed probability for node pairs that - // successfully relayed the previous attempt. - prevSuccessProbability float64 -} - -// getNodeProbability calculates the probability for connections from a node -// that have not been tried before. The results parameter is a list of last -// payment results for that node. -func (p *probabilityEstimator) getNodeProbability(now time.Time, - results NodeResults, amt lnwire.MilliSatoshi, - capacity btcutil.Amount) float64 { - - // We reduce the apriori hop probability if the amount comes close to - // the capacity. - apriori := p.AprioriHopProbability * capacityFactor(amt, capacity) - - // If the channel history is not to be taken into account, we can return - // early here with the configured a priori probability. - if p.AprioriWeight == 1 { - return apriori - } - - // If there is no channel history, our best estimate is still the a - // priori probability. - if len(results) == 0 { - return apriori - } - - // The value of the apriori weight is in the range [0, 1]. Convert it to - // a factor that properly expresses the intention of the weight in the - // following weight average calculation. When the apriori weight is 0, - // the apriori factor is also 0. This means it won't have any effect on - // the weighted average calculation below. When the apriori weight - // approaches 1, the apriori factor goes to infinity. It will heavily - // outweigh any observations that have been collected. - aprioriFactor := 1/(1-p.AprioriWeight) - 1 - - // Calculate a weighted average consisting of the apriori probability - // and historical observations. This is the part that incentivizes nodes - // to make sure that all (not just some) of their channels are in good - // shape. Senders will steer around nodes that have shown a few - // failures, even though there may be many channels still untried. - // - // If there is just a single observation and the apriori weight is 0, - // this single observation will totally determine the node probability. - // The node probability is returned for all other channels of the node. - // This means that one failure will lead to the success probability - // estimates for all other channels being 0 too. The probability for the - // channel that was tried will not even recover, because it is - // recovering to the node probability (which is zero). So one failure - // effectively prunes all channels of the node forever. This is the most - // aggressive way in which we can penalize nodes and unlikely to yield - // good results in a real network. - probabilitiesTotal := apriori * aprioriFactor - totalWeight := aprioriFactor - - for _, result := range results { - switch { - // Weigh success with a constant high weight of 1. There is no - // decay. Amt is never zero, so this clause is never executed - // when result.SuccessAmt is zero. - case amt <= result.SuccessAmt: - totalWeight++ - probabilitiesTotal += p.prevSuccessProbability - - // Weigh failures in accordance with their age. The base - // probability of a failure is considered zero, so nothing needs - // to be added to probabilitiesTotal. - case !result.FailTime.IsZero() && amt >= result.FailAmt: - age := now.Sub(result.FailTime) - totalWeight += p.getWeight(age) - } - } - - return probabilitiesTotal / totalWeight -} - -// getWeight calculates a weight in the range [0, 1] that should be assigned to -// a payment result. Weight follows an exponential curve that starts at 1 when -// the result is fresh and asymptotically approaches zero over time. The rate at -// which this happens is controlled by the penaltyHalfLife parameter. -func (p *probabilityEstimator) getWeight(age time.Duration) float64 { - exp := -age.Hours() / p.PenaltyHalfLife.Hours() - return math.Pow(2, exp) -} - -// capacityFactor is a multiplier that can be used to reduce the probability -// depending on how much of the capacity is sent. The limits are 1 for amt == 0 -// and 0 for amt >> cutoffMsat. The function drops significantly when amt -// reaches cutoffMsat. smearingMsat determines over which scale the reduction -// takes place. -func capacityFactor(amt lnwire.MilliSatoshi, capacity btcutil.Amount) float64 { - // If we don't have information about the capacity, which can be the - // case for hop hints or local channels, we return unity to not alter - // anything. - if capacity == 0 { - return 1.0 - } - - capMsat := float64(lnwire.NewMSatFromSatoshis(capacity)) - amtMsat := float64(amt) - - if amtMsat > capMsat { - return 0 - } - - cutoffMsat := capacityCutoffFraction * capMsat - smearingMsat := capacitySmearingFraction * capMsat - - // We compute a logistic function mirrored around the y axis, centered - // at cutoffMsat, decaying over the smearingMsat scale. - denominator := 1 + math.Exp(-(amtMsat-cutoffMsat)/smearingMsat) - - return 1 - 1/denominator -} - -// getPairProbability estimates the probability of successfully traversing to -// toNode based on historical payment outcomes for the from node. Those outcomes -// are passed in via the results parameter. -func (p *probabilityEstimator) getPairProbability( - now time.Time, results NodeResults, toNode route.Vertex, - amt lnwire.MilliSatoshi, capacity btcutil.Amount) float64 { - - nodeProbability := p.getNodeProbability(now, results, amt, capacity) - - return p.calculateProbability( - now, results, nodeProbability, toNode, amt, - ) -} - -// getLocalPairProbability estimates the probability of successfully traversing -// our own local channels to toNode. -func (p *probabilityEstimator) getLocalPairProbability( - now time.Time, results NodeResults, toNode route.Vertex) float64 { - - // For local channels that have never been tried before, we assume them - // to be successful. We have accurate balance and online status - // information on our own channels, so when we select them in a route it - // is close to certain that those channels will work. - nodeProbability := p.prevSuccessProbability - - return p.calculateProbability( - now, results, nodeProbability, toNode, lnwire.MaxMilliSatoshi, - ) -} - -// calculateProbability estimates the probability of successfully traversing to -// toNode based on historical payment outcomes and a fall-back node probability. -func (p *probabilityEstimator) calculateProbability( - now time.Time, results NodeResults, - nodeProbability float64, toNode route.Vertex, - amt lnwire.MilliSatoshi) float64 { - - // Retrieve the last pair outcome. - lastPairResult, ok := results[toNode] - - // If there is no history for this pair, return the node probability - // that is a probability estimate for untried channel. - if !ok { - return nodeProbability - } - - // For successes, we have a fixed (high) probability. Those pairs will - // be assumed good until proven otherwise. Amt is never zero, so this - // clause is never executed when lastPairResult.SuccessAmt is zero. - if amt <= lastPairResult.SuccessAmt { - return p.prevSuccessProbability - } - - // Take into account a minimum penalize amount. For balance errors, a - // failure may be reported with such a minimum to prevent too aggressive - // penalization. If the current amount is smaller than the amount that - // previously triggered a failure, we act as if this is an untried - // channel. - if lastPairResult.FailTime.IsZero() || amt < lastPairResult.FailAmt { - return nodeProbability - } - - timeSinceLastFailure := now.Sub(lastPairResult.FailTime) - - // Calculate success probability based on the weight of the last - // failure. When the failure is fresh, its weight is 1 and we'll return - // probability 0. Over time the probability recovers to the node - // probability. It would be as if this channel was never tried before. - weight := p.getWeight(timeSinceLastFailure) - probability := nodeProbability * (1 - weight) - - return probability +// estimatorConfig represents a configuration for a probability estimator. +type estimatorConfig interface { + // validate checks that all configuration parameters are sane. + validate() error } diff --git a/routing/probability_estimator_test.go b/routing/probability_estimator_test.go index 2e683fc2d..3f6fa3511 100644 --- a/routing/probability_estimator_test.go +++ b/routing/probability_estimator_test.go @@ -1,281 +1,95 @@ package routing import ( + "math" "testing" "time" - "github.com/btcsuite/btcd/btcutil" - "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/routing/route" - "github.com/stretchr/testify/require" ) -const ( - // Define node identifiers. - node1 = 1 - node2 = 2 - node3 = 3 - - // untriedNode is a node id for which we don't record any results in - // this test. This can be used to assert the probability for untried - // ndoes. - untriedNode = 255 - - // Define test estimator parameters. - aprioriHopProb = 0.6 - aprioriWeight = 0.75 - aprioriPrevSucProb = 0.95 - - // testCapacity is used to define a capacity for some channels. - testCapacity = btcutil.Amount(100_000) - testAmount = lnwire.MilliSatoshi(50_000_000) - - // Defines the capacityFactor for testAmount and testCapacity. - capFactor = 0.9241 -) - -type estimatorTestContext struct { - t *testing.T - estimator *probabilityEstimator - - // results contains a list of last results. Every element in the list - // corresponds to the last result towards a node. The list index equals - // the node id. So the first element in the list is the result towards - // node 0. - results map[int]TimedPairResult +// Create a set of test results. +var resultTime = time.Unix(1674169200, 0) // 20.01.2023 +var now = time.Unix(1674190800, 0) // 6 hours later +var results = NodeResults{ + route.Vertex{byte(0)}: TimedPairResult{ + FailAmt: 200_000_000, + FailTime: resultTime, + SuccessAmt: 100_000_000, + SuccessTime: resultTime, + }, + route.Vertex{byte(1)}: TimedPairResult{ + FailAmt: 200_000_000, + FailTime: resultTime, + SuccessAmt: 100_000_000, + SuccessTime: resultTime, + }, + route.Vertex{byte(2)}: TimedPairResult{ + FailAmt: 200_000_000, + FailTime: resultTime, + SuccessAmt: 100_000_000, + SuccessTime: resultTime, + }, + route.Vertex{byte(3)}: TimedPairResult{ + FailAmt: 200_000_000, + FailTime: resultTime, + SuccessAmt: 100_000_000, + SuccessTime: resultTime, + }, + route.Vertex{byte(4)}: TimedPairResult{ + FailAmt: 200_000_000, + FailTime: resultTime, + SuccessAmt: 100_000_000, + SuccessTime: resultTime, + }, } -func newEstimatorTestContext(t *testing.T) *estimatorTestContext { - return &estimatorTestContext{ - t: t, - estimator: &probabilityEstimator{ - ProbabilityEstimatorCfg: ProbabilityEstimatorCfg{ - AprioriHopProbability: aprioriHopProb, - AprioriWeight: aprioriWeight, - PenaltyHalfLife: time.Hour, - }, - prevSuccessProbability: aprioriPrevSucProb, - }, - } -} +// probability is a package level variable to prevent the compiler from +// optimizing the benchmark. +var probability float64 -// assertPairProbability asserts that the calculated success probability is -// correct. -func (c *estimatorTestContext) assertPairProbability(now time.Time, - toNode byte, amt lnwire.MilliSatoshi, capacity btcutil.Amount, - expectedProb float64) { - - c.t.Helper() - - results := make(NodeResults) - for i, r := range c.results { - results[route.Vertex{byte(i)}] = r - } - - const tolerance = 0.01 - - p := c.estimator.getPairProbability( - now, results, route.Vertex{toNode}, amt, capacity, - ) - diff := p - expectedProb - if diff > tolerance || diff < -tolerance { - c.t.Fatalf("expected probability %v for node %v, but got %v", - expectedProb, toNode, p) - } -} - -// TestProbabilityEstimatorNoResults tests the probability estimation when no -// results are available. -func TestProbabilityEstimatorNoResults(t *testing.T) { - ctx := newEstimatorTestContext(t) - - // A zero amount does not trigger capacity rescaling. - ctx.assertPairProbability( - testTime, 0, 0, testCapacity, aprioriHopProb, - ) - - // We expect a reduced probability when a higher amount is used. - expected := aprioriHopProb * capFactor - ctx.assertPairProbability( - testTime, 0, testAmount, testCapacity, expected, - ) -} - -// TestProbabilityEstimatorOneSuccess tests the probability estimation for nodes -// that have a single success result. -func TestProbabilityEstimatorOneSuccess(t *testing.T) { - ctx := newEstimatorTestContext(t) - - ctx.results = map[int]TimedPairResult{ - node1: { - SuccessAmt: testAmount, +// BenchmarkBimodalPairProbability benchmarks the probability calculation. +func BenchmarkBimodalPairProbability(b *testing.B) { + estimator := BimodalEstimator{ + BimodalConfig: BimodalConfig{ + BimodalScaleMsat: scale, + BimodalNodeWeight: 0.2, + BimodalDecayTime: 48 * time.Hour, }, } - // Because of the previous success, this channel keep reporting a high - // probability. - ctx.assertPairProbability( - testTime, node1, 100, testCapacity, aprioriPrevSucProb, - ) - - // The apriori success probability indicates that in the past we were - // able to send the full amount. We don't want to reduce this - // probability with the capacity factor, which is tested here. - ctx.assertPairProbability( - testTime, node1, testAmount, testCapacity, aprioriPrevSucProb, - ) - - // Untried channels are also influenced by the success. With a - // aprioriWeight of 0.75, the a priori probability is assigned weight 3. - expectedP := (3*aprioriHopProb + 1*aprioriPrevSucProb) / 4 - ctx.assertPairProbability( - testTime, untriedNode, 100, testCapacity, expectedP, - ) - - // Check that the correct probability is computed for larger amounts. - apriori := aprioriHopProb * capFactor - - expectedP = (3*apriori + 1*aprioriPrevSucProb) / 4 - ctx.assertPairProbability( - testTime, untriedNode, testAmount, testCapacity, expectedP, - ) + toNode := route.Vertex{byte(0)} + var p float64 + for i := 0; i < b.N; i++ { + p = estimator.PairProbability(now, results, toNode, + 150_000_000, 300_000) + } + probability = p } -// TestProbabilityEstimatorOneFailure tests the probability estimation for nodes -// that have a single failure. -func TestProbabilityEstimatorOneFailure(t *testing.T) { - ctx := newEstimatorTestContext(t) - - ctx.results = map[int]TimedPairResult{ - node1: { - FailTime: testTime.Add(-time.Hour), - FailAmt: lnwire.MilliSatoshi(50), +// BenchmarkAprioriPairProbability benchmarks the probability calculation. +func BenchmarkAprioriPairProbability(b *testing.B) { + estimator := AprioriEstimator{ + AprioriConfig: AprioriConfig{ + AprioriWeight: 0.2, + PenaltyHalfLife: 48 * time.Hour, + AprioriHopProbability: 0.5, }, } - // For an untried node, we expected the node probability. The weight for - // the failure after one hour is 0.5. This makes the node probability - // 0.51: - expectedNodeProb := (3*aprioriHopProb + 0.5*0) / 3.5 - ctx.assertPairProbability( - testTime, untriedNode, 100, testCapacity, expectedNodeProb, - ) - - // The pair probability decays back to the node probability. With the - // weight at 0.5, we expected a pair probability of 0.5 * 0.51 = 0.25. - ctx.assertPairProbability( - testTime, node1, 100, testCapacity, expectedNodeProb/2, - ) + toNode := route.Vertex{byte(0)} + var p float64 + for i := 0; i < b.N; i++ { + p = estimator.PairProbability(now, results, toNode, + 150_000_000, 300_000) + } + probability = p } -// TestProbabilityEstimatorMix tests the probability estimation for nodes for -// which a mix of successes and failures is recorded. -func TestProbabilityEstimatorMix(t *testing.T) { - ctx := newEstimatorTestContext(t) - - ctx.results = map[int]TimedPairResult{ - node1: { - SuccessAmt: lnwire.MilliSatoshi(1000), - }, - node2: { - FailTime: testTime.Add(-2 * time.Hour), - FailAmt: lnwire.MilliSatoshi(50), - }, - node3: { - FailTime: testTime.Add(-3 * time.Hour), - FailAmt: lnwire.MilliSatoshi(50), - }, - } - - // We expect the probability for a previously successful channel to - // remain high. - ctx.assertPairProbability( - testTime, node1, 100, testCapacity, prevSuccessProbability, - ) - - // For an untried node, we expected the node probability to be returned. - // This is a weighted average of the results above and the a priori - // probability: 0.62. - expectedNodeProb := (3*aprioriHopProb + 1*prevSuccessProbability) / - (3 + 1 + 0.25 + 0.125) - - ctx.assertPairProbability( - testTime, untriedNode, 100, testCapacity, expectedNodeProb, - ) - - // For the previously failed connection with node 1, we expect 0.75 * - // the node probability = 0.47. - ctx.assertPairProbability( - testTime, node2, 100, testCapacity, expectedNodeProb*0.75, - ) -} - -// TestCapacityCutoff tests the mathematical expression and limits for the -// capacity factor. -func TestCapacityCutoff(t *testing.T) { - t.Parallel() - - capacitySat := 1_000_000 - capacityMSat := capacitySat * 1000 - - tests := []struct { - name string - amountMsat int - expectedFactor float64 - }{ - { - name: "zero amount", - expectedFactor: 1, - }, - { - name: "low amount", - amountMsat: capacityMSat / 10, - expectedFactor: 0.998, - }, - { - name: "half amount", - amountMsat: capacityMSat / 2, - expectedFactor: 0.924, - }, - { - name: "cutoff amount", - amountMsat: int( - capacityCutoffFraction * float64(capacityMSat), - ), - expectedFactor: 0.5, - }, - { - name: "high amount", - amountMsat: capacityMSat * 80 / 100, - expectedFactor: 0.377, - }, - { - // Even when we spend the full capacity, we still want - // to have some residual probability to not throw away - // routes due to a min probability requirement of the - // whole path. - name: "full amount", - amountMsat: capacityMSat, - expectedFactor: 0.076, - }, - { - name: "more than capacity", - amountMsat: capacityMSat + 1, - expectedFactor: 0.0, - }, - } - - for _, test := range tests { - test := test - - t.Run(test.name, func(t *testing.T) { - t.Parallel() - - got := capacityFactor( - lnwire.MilliSatoshi(test.amountMsat), - btcutil.Amount(capacitySat), - ) - require.InDelta(t, test.expectedFactor, got, 0.001) - }) +// BenchmarkExp benchmarks the exponential function as provided by the math +// library. +func BenchmarkExp(b *testing.B) { + for i := 0; i < b.N; i++ { + math.Exp(0.1) } } diff --git a/routing/router_test.go b/routing/router_test.go index 0346721e3..8701d8b28 100644 --- a/routing/router_test.go +++ b/routing/router_test.go @@ -119,13 +119,15 @@ func createTestCtxFromGraphInstanceAssumeValid(t *testing.T, AttemptCost: 100, } - mcConfig := &MissionControlConfig{ - ProbabilityEstimatorCfg: ProbabilityEstimatorCfg{ - PenaltyHalfLife: time.Hour, - AprioriHopProbability: 0.9, - AprioriWeight: 0.5, - }, + aCfg := AprioriConfig{ + PenaltyHalfLife: time.Hour, + AprioriHopProbability: 0.9, + AprioriWeight: 0.5, } + estimator, err := NewAprioriEstimator(aCfg) + require.NoError(t, err) + + mcConfig := &MissionControlConfig{Estimator: estimator} mc, err := NewMissionControl( graphInstance.graphBackend, route.Vertex{}, mcConfig, diff --git a/sample-lnd.conf b/sample-lnd.conf index e3fc5d08f..96ac7e015 100644 --- a/sample-lnd.conf +++ b/sample-lnd.conf @@ -1105,21 +1105,23 @@ litecoin.node=ltcd [routerrpc] +; Probability estimator used for pathfinding. (default: apriori) +; routerrpc.estimator=[apriori|bimodal] + ; Minimum required route success probability to attempt the payment (default: ; 0.01) ; routerrpc.minrtprob=1 -; Assumed success probability of a hop in a route when no other information is -; available. (default: 0.6) -; routerrpc.apriorihopprob=0.2 +; The maximum number of payment results that are held on disk by mission control +; (default: 1000) +; routerrpc.maxmchistory=900 -; Weight of the a priori probability in success probability estimation. Valid -; values are in [0, 1]. (default: 0.5) -; routerrpc.aprioriweight=0.3 +; The time interval with which the MC store state is flushed to the database +; (default: 1s) +; routerrpc.mcflushinterval=1m -; Defines the duration after which a penalized node or channel is back at 50% -; probability (default: 1h0m0s) -; routerrpc.penaltyhalflife=2h +; Path to the router macaroon +; routerrpc.routermacaroonpath=~/.lnd/data/chain/bitcoin/simnet/router.macaroon ; The (virtual) fixed cost in sats of a failed payment attempt (default: 100) ; routerrpc.attemptcost=90 @@ -1128,15 +1130,32 @@ litecoin.node=ltcd ; attempt (default: 1000) ; routerrpc.attemptcostppm=900 -; The maximum number of payment results that are held on disk by mission control -; (default: 1000) -; routerrpc.maxmchistory=900 +; Assumed success probability of a hop in a route when no other information is +; available. (default: 0.6) +; routerrpc.apriori.hopprob=0.2 -; The time interval with which the MC store state is flushed to the DB. -; routerrpc.mcflushinterval=1m +; Weight of the a priori probability in success probability estimation. Valid +; values are in [0, 1]. (default: 0.5) +; routerrpc.apriori.weight=0.3 -; Path to the router macaroon -; routerrpc.routermacaroonpath=~/.lnd/data/chain/bitcoin/simnet/router.macaroon +; Defines the duration after which a penalized node or channel is back at 50% +; probability (default: 1h0m0s) +; routerrpc.apriori.penaltyhalflife=2h + +; Describes the scale over which channels still have some liquidity left on +; both channel ends. A very low value (compared to typical channel capacities) +; means that we assume unbalanced channels, a very high value means randomly +; balanced channels. Value in msat. (default: 300000000) +; routerrpc.bimodal.scale=1000000000 + +; Defines how strongly non-routed channels of forwarders should be taken into +; account for probability estimation. A weight of zero disables this feature. +; Valid values are in [0, 1]. (default: 0.2) +; routerrpc.bimodal.nodeweight=0.3 + +; Defines the information decay of knowledge about previous successes and +; failures in channels. (default: 168h0m0s) +; routerrpc.bimodal.decaytime=72h [workers] diff --git a/server.go b/server.go index 8a3d6fedc..d63d9644d 100644 --- a/server.go +++ b/server.go @@ -868,20 +868,58 @@ func newServer(cfg *Config, listenAddrs []net.Addr, // servers, the mission control instance itself can be moved there too. routingConfig := routerrpc.GetRoutingConfig(cfg.SubRPCServers.RouterRPC) - estimatorCfg := routing.ProbabilityEstimatorCfg{ - AprioriHopProbability: routingConfig.AprioriHopProbability, - PenaltyHalfLife: routingConfig.PenaltyHalfLife, - AprioriWeight: routingConfig.AprioriWeight, + // We only initialize a probability estimator if there's no custom one. + var estimator routing.Estimator + if cfg.Estimator != nil { + estimator = cfg.Estimator + } else { + switch routingConfig.ProbabilityEstimatorType { + case routing.AprioriEstimatorName: + aCfg := routingConfig.AprioriConfig + aprioriConfig := routing.AprioriConfig{ + AprioriHopProbability: aCfg.HopProbability, + PenaltyHalfLife: aCfg.PenaltyHalfLife, + AprioriWeight: aCfg.Weight, + } + + estimator, err = routing.NewAprioriEstimator( + aprioriConfig, + ) + if err != nil { + return nil, err + } + + case routing.BimodalEstimatorName: + bCfg := routingConfig.BimodalConfig + bimodalConfig := routing.BimodalConfig{ + BimodalNodeWeight: bCfg.NodeWeight, + BimodalScaleMsat: lnwire.MilliSatoshi( + bCfg.Scale, + ), + BimodalDecayTime: bCfg.DecayTime, + } + + estimator, err = routing.NewBimodalEstimator( + bimodalConfig, + ) + if err != nil { + return nil, err + } + + default: + return nil, fmt.Errorf("unknown estimator type %v", + routingConfig.ProbabilityEstimatorType) + } } + mcCfg := &routing.MissionControlConfig{ + Estimator: estimator, + MaxMcHistory: routingConfig.MaxMcHistory, + McFlushInterval: routingConfig.McFlushInterval, + MinFailureRelaxInterval: routing.DefaultMinFailureRelaxInterval, + } s.missionControl, err = routing.NewMissionControl( - dbs.ChanStateDB, selfNode.PubKeyBytes, - &routing.MissionControlConfig{ - ProbabilityEstimatorCfg: estimatorCfg, - MaxMcHistory: routingConfig.MaxMcHistory, - McFlushInterval: routingConfig.McFlushInterval, - MinFailureRelaxInterval: routing.DefaultMinFailureRelaxInterval, - }, + dbs.ChanStateDB, selfNode.PubKeyBytes, mcCfg, ) if err != nil { return nil, fmt.Errorf("can't create mission control: %v", err)