Repository: lightningnetwork/lnd
Pull request #3462 (2019-09-04 16:20:28)

routing+routerrpc: improve prob. estimation for untried connections
This PR changes mission control to partially base the estimated probability for untried connections on historical results obtained in previous payment attempts. This prevents the payment loop from getting stuck in an area of the network that is not of good quality. It also incentivizes routing nodes to keep all
of their channels in good shape.

The configuration parameter `--routerrpc.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 1 will completely ignore historical results and always assume the configured a priori probability for untried connections. A value of 0 will ignore the a priori probability completely and only base the probability on historical results, unless there are none available.

Using this parameter, senders can configure how aggressive nodes should be penalized. A low a priori weight will make mission control switch quickly to using a different node if a tried node doesn't perform well. The downside of this is that the sender may miss out on low fee channels that could possibly complete the payment successfully.

Indirectly, the collective of senders also shapes the network with their settings of this parameter. When senders start to demand a higher performance from routing nodes, routing nodes need to meet that expectation in order to keep generating revenue.

joostjager: Did a bit of performance testing with and without these changes. With a clean mission control history, there is no noticeable difference. This is expected, because there are hardly iterations in the loop inside `GetNodeProbability`. For the test call to `QueryRoutes`, 330 nodes were visited and 7661 edges expanded.

To see a performance effect, it is probably needed to have a fully loaded mission control history. It is a question how likely this situation is, especially on low-powered devices where pathfinding performance becomes important. There is also a default limit to the mission control history of 1000 past payments, although this can be changed.

I tried a simulation of a full mission control by inserting 10 fake results for every node visited, but didn't see an effect either. Db access during path finding is likely still where time is mainly spent.

I'd propose to not prematurely add optimizations like caching of the node probability in this pr and first see what feedback we get on it from users. I've added another commit that logs useful performance metrics.
joostjager: > Completed an initial surface-level pass. Will do another one focusing on the core estimation changes to ensure I fully grok them. Overall I think this is a step in the right direction, but wonder how we can test these assumptions in the wild, and also if it's too early to make a change like this given that as we know it, node operators aren't very sophisticated _yet_.

We can always set the default a priori weight quite high, so the effect of this will be light initially. Give some time for users and operators to become aware of the new scoring.
joostjager: I realized that the calculation of the node probability should also be amount independent. Especially if we make the amount more important in the follow up pr #3493. Updated `GetNodeProbability`. I also removed those precalculated probabilities from the mc state dump and provided a new rpc to query a specific probability.
joostjager: Move new `querymcprob` call to #3556 

Patch:
diff --git a/cmd/lncli/cmd_query_mission_control.go b/cmd/lncli/cmd_query_mission_control.go
index 3c13a09b8..0122fb023 100644
--- a/cmd/lncli/cmd_query_mission_control.go
+++ b/cmd/lncli/cmd_query_mission_control.go
@@ -31,36 +31,17 @@ func queryMissionControl(ctx *cli.Context) error {
 		return err
 	}
 
-	type displayNodeHistory struct {
-		Pubkey           string
-		LastFailTime     int64
-		OtherSuccessProb float32
-	}
-
 	type displayPairHistory struct {
 		NodeFrom, NodeTo      string
 		LastAttemptSuccessful bool
 		Timestamp             int64
-		SuccessProb           float32
 		MinPenalizeAmtSat     int64
 	}
 
 	displayResp := struct {
-		Nodes []displayNodeHistory
 		Pairs []displayPairHistory
 	}{}
 
-	for _, n := range snapshot.Nodes {
-		displayResp.Nodes = append(
-			displayResp.Nodes,
-			displayNodeHistory{
-				Pubkey:           hex.EncodeToString(n.Pubkey),
-				LastFailTime:     n.LastFailTime,
-				OtherSuccessProb: n.OtherSuccessProb,
-			},
-		)
-	}
-
 	for _, n := range snapshot.Pairs {
 		displayResp.Pairs = append(
 			displayResp.Pairs,
@@ -69,7 +50,6 @@ func queryMissionControl(ctx *cli.Context) error {
 				NodeTo:                hex.EncodeToString(n.NodeTo),
 				LastAttemptSuccessful: n.LastAttemptSuccessful,
 				Timestamp:             n.Timestamp,
-				SuccessProb:           n.SuccessProb,
 				MinPenalizeAmtSat:     n.MinPenalizeAmtSat,
 			},
 		)
diff --git a/lnrpc/routerrpc/config.go b/lnrpc/routerrpc/config.go
index ba094c8f0..98b3594cf 100644
--- a/lnrpc/routerrpc/config.go
+++ b/lnrpc/routerrpc/config.go
@@ -16,6 +16,15 @@ type RoutingConfig struct {
 	// 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"`
diff --git a/lnrpc/routerrpc/config_active.go b/lnrpc/routerrpc/config_active.go
index 0479211ca..a8393fc61 100644
--- a/lnrpc/routerrpc/config_active.go
+++ b/lnrpc/routerrpc/config_active.go
@@ -45,6 +45,7 @@ type Config struct {
 func DefaultConfig() *Config {
 	defaultRoutingConfig := RoutingConfig{
 		AprioriHopProbability: routing.DefaultAprioriHopProbability,
+		AprioriWeight:         routing.DefaultAprioriWeight,
 		MinRouteProbability:   routing.DefaultMinRouteProbability,
 		PenaltyHalfLife:       routing.DefaultPenaltyHalfLife,
 		AttemptCost: routing.DefaultPaymentAttemptPenalty.
@@ -61,6 +62,7 @@ func DefaultConfig() *Config {
 func GetRoutingConfig(cfg *Config) *RoutingConfig {
 	return &RoutingConfig{
 		AprioriHopProbability: cfg.AprioriHopProbability,
+		AprioriWeight:         cfg.AprioriWeight,
 		MinRouteProbability:   cfg.MinRouteProbability,
 		AttemptCost:           cfg.AttemptCost,
 		PenaltyHalfLife:       cfg.PenaltyHalfLife,
diff --git a/lnrpc/routerrpc/config_default.go b/lnrpc/routerrpc/config_default.go
index f8af98f34..6fcc74a77 100644
--- a/lnrpc/routerrpc/config_default.go
+++ b/lnrpc/routerrpc/config_default.go
@@ -18,6 +18,7 @@ func DefaultConfig() *Config {
 func GetRoutingConfig(cfg *Config) *RoutingConfig {
 	return &RoutingConfig{
 		AprioriHopProbability: routing.DefaultAprioriHopProbability,
+		AprioriWeight:         routing.DefaultAprioriWeight,
 		MinRouteProbability:   routing.DefaultMinRouteProbability,
 		AttemptCost: routing.DefaultPaymentAttemptPenalty.
 			ToSatoshis(),
diff --git a/lnrpc/routerrpc/router.pb.go b/lnrpc/routerrpc/router.pb.go
index 0fe976a4c..5463ee709 100644
--- a/lnrpc/routerrpc/router.pb.go
+++ b/lnrpc/routerrpc/router.pb.go
@@ -1006,8 +1006,6 @@ var xxx_messageInfo_QueryMissionControlRequest proto.InternalMessageInfo
 
 /// QueryMissionControlResponse contains mission control state.
 type QueryMissionControlResponse struct {
-	/// Node-level mission control state.
-	Nodes []*NodeHistory `protobuf:"bytes,1,rep,name=nodes,proto3" json:"nodes,omitempty"`
 	/// Node pair-level mission control state.
 	Pairs                []*PairHistory `protobuf:"bytes,2,rep,name=pairs,proto3" json:"pairs,omitempty"`
 	XXX_NoUnkeyedLiteral struct{}       `json:"-"`
@@ -1040,13 +1038,6 @@ func (m *QueryMissionControlResponse) XXX_DiscardUnknown() {
 
 var xxx_messageInfo_QueryMissionControlResponse proto.InternalMessageInfo
 
-func (m *QueryMissionControlResponse) GetNodes() []*NodeHistory {
-	if m != nil {
-		return m.Nodes
-	}
-	return nil
-}
-
 func (m *QueryMissionControlResponse) GetPairs() []*PairHistory {
 	if m != nil {
 		return m.Pairs
@@ -1054,67 +1045,6 @@ func (m *QueryMissionControlResponse) GetPairs() []*PairHistory {
 	return nil
 }
 
-/// NodeHistory contains the mission control state for a particular node.
-type NodeHistory struct {
-	/// Node pubkey
-	Pubkey []byte `protobuf:"bytes,1,opt,name=pubkey,proto3" json:"pubkey,omitempty"`
-	/// Time stamp of last failure. Set to zero if no failure happened yet.
-	LastFailTime int64 `protobuf:"varint,2,opt,name=last_fail_time,proto3" json:"last_fail_time,omitempty"`
-	//*
-	//Estimation of success probability of forwarding towards peers of this node
-	//for which no specific history is available.
-	OtherSuccessProb     float32  `protobuf:"fixed32,3,opt,name=other_success_prob,proto3" json:"other_success_prob,omitempty"`
-	XXX_NoUnkeyedLiteral struct{} `json:"-"`
-	XXX_unrecognized     []byte   `json:"-"`
-	XXX_sizecache        int32    `json:"-"`
-}
-
-func (m *NodeHistory) Reset()         { *m = NodeHistory{} }
-func (m *NodeHistory) String() string { return proto.CompactTextString(m) }
-func (*NodeHistory) ProtoMessage()    {}
-func (*NodeHistory) Descriptor() ([]byte, []int) {
-	return fileDescriptor_7a0613f69d37b0a5, []int{13}
-}
-
-func (m *NodeHistory) XXX_Unmarshal(b []byte) error {
-	return xxx_messageInfo_NodeHistory.Unmarshal(m, b)
-}
-func (m *NodeHistory) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
-	return xxx_messageInfo_NodeHistory.Marshal(b, m, deterministic)
-}
-func (m *NodeHistory) XXX_Merge(src proto.Message) {
-	xxx_messageInfo_NodeHistory.Merge(m, src)
-}
-func (m *NodeHistory) XXX_Size() int {
-	return xxx_messageInfo_NodeHistory.Size(m)
-}
-func (m *NodeHistory) XXX_DiscardUnknown() {
-	xxx_messageInfo_NodeHistory.DiscardUnknown(m)
-}
-
-var xxx_messageInfo_NodeHistory proto.InternalMessageInfo
-
-func (m *NodeHistory) GetPubkey() []byte {
-	if m != nil {
-		return m.Pubkey
-	}
-	return nil
-}
-
-func (m *NodeHistory) GetLastFailTime() int64 {
-	if m != nil {
-		return m.LastFailTime
-	}
-	return 0
-}
-
-func (m *NodeHistory) GetOtherSuccessProb() float32 {
-	if m != nil {
-		return m.OtherSuccessProb
-	}
-	return 0
-}
-
 /// PairHistory contains the mission control state for a particular node pair.
 type PairHistory struct {
 	/// The source node pubkey of the pair.
@@ -1125,8 +1055,6 @@ type PairHistory struct {
 	Timestamp int64 `protobuf:"varint,3,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
 	/// Minimum penalization amount (only applies to failed attempts).
 	MinPenalizeAmtSat int64 `protobuf:"varint,4,opt,name=min_penalize_amt_sat,proto3" json:"min_penalize_amt_sat,omitempty"`
-	/// Estimation of success probability for this pair.
-	SuccessProb float32 `protobuf:"fixed32,5,opt,name=success_prob,proto3" json:"success_prob,omitempty"`
 	/// Whether the last payment attempt through this pair was successful.
 	LastAttemptSuccessful bool     `protobuf:"varint,6,opt,name=last_attempt_successful,proto3" json:"last_attempt_successful,omitempty"`
 	XXX_NoUnkeyedLiteral  struct{} `json:"-"`
@@ -1138,7 +1066,7 @@ func (m *PairHistory) Reset()         { *m = PairHistory{} }
 func (m *PairHistory) String() string { return proto.CompactTextString(m) }
 func (*PairHistory) ProtoMessage()    {}
 func (*PairHistory) Descriptor() ([]byte, []int) {
-	return fileDescriptor_7a0613f69d37b0a5, []int{14}
+	return fileDescriptor_7a0613f69d37b0a5, []int{13}
 }
 
 func (m *PairHistory) XXX_Unmarshal(b []byte) error {
@@ -1187,13 +1115,6 @@ func (m *PairHistory) GetMinPenalizeAmtSat() int64 {
 	return 0
 }
 
-func (m *PairHistory) GetSuccessProb() float32 {
-	if m != nil {
-		return m.SuccessProb
-	}
-	return 0
-}
-
 func (m *PairHistory) GetLastAttemptSuccessful() bool {
 	if m != nil {
 		return m.LastAttemptSuccessful
@@ -1227,7 +1148,7 @@ func (m *BuildRouteRequest) Reset()         { *m = BuildRouteRequest{} }
 func (m *BuildRouteRequest) String() string { return proto.CompactTextString(m) }
 func (*BuildRouteRequest) ProtoMessage()    {}
 func (*BuildRouteRequest) Descriptor() ([]byte, []int) {
-	return fileDescriptor_7a0613f69d37b0a5, []int{15}
+	return fileDescriptor_7a0613f69d37b0a5, []int{14}
 }
 
 func (m *BuildRouteRequest) XXX_Unmarshal(b []byte) error {
@@ -1289,7 +1210,7 @@ func (m *BuildRouteResponse) Reset()         { *m = BuildRouteResponse{} }
 func (m *BuildRouteResponse) String() string { return proto.CompactTextString(m) }
 func (*BuildRouteResponse) ProtoMessage()    {}
 func (*BuildRouteResponse) Descriptor() ([]byte, []int) {
-	return fileDescriptor_7a0613f69d37b0a5, []int{16}
+	return fileDescriptor_7a0613f69d37b0a5, []int{15}
 }
 
 func (m *BuildRouteResponse) XXX_Unmarshal(b []byte) error {
@@ -1334,7 +1255,6 @@ func init() {
 	proto.RegisterType((*ResetMissionControlResponse)(nil), "routerrpc.ResetMissionControlResponse")
 	proto.RegisterType((*QueryMissionControlRequest)(nil), "routerrpc.QueryMissionControlRequest")
 	proto.RegisterType((*QueryMissionControlResponse)(nil), "routerrpc.QueryMissionControlResponse")
-	proto.RegisterType((*NodeHistory)(nil), "routerrpc.NodeHistory")
 	proto.RegisterType((*PairHistory)(nil), "routerrpc.PairHistory")
 	proto.RegisterType((*BuildRouteRequest)(nil), "routerrpc.BuildRouteRequest")
 	proto.RegisterType((*BuildRouteResponse)(nil), "routerrpc.BuildRouteResponse")
@@ -1343,125 +1263,120 @@ func init() {
 func init() { proto.RegisterFile("routerrpc/router.proto", fileDescriptor_7a0613f69d37b0a5) }
 
 var fileDescriptor_7a0613f69d37b0a5 = []byte{
-	// 1875 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x58, 0xd1, 0x72, 0x22, 0xb9,
-	0xd5, 0xde, 0x36, 0x60, 0xe0, 0x00, 0x76, 0x5b, 0xf6, 0x78, 0x7a, 0xb0, 0xbd, 0xe3, 0x65, 0xf7,
-	0x9f, 0x75, 0x4d, 0xcd, 0x6f, 0x4f, 0x9c, 0xda, 0xad, 0xa9, 0xbd, 0x48, 0x0a, 0x83, 0x58, 0xf7,
-	0x0c, 0x34, 0x5e, 0x01, 0xb3, 0x3b, 0xc9, 0x85, 0x4a, 0x06, 0xd9, 0x74, 0xb9, 0xe9, 0x66, 0xbb,
-	0x85, 0x33, 0xce, 0x45, 0xaa, 0x52, 0xb9, 0xce, 0x73, 0xe4, 0x26, 0x77, 0x79, 0x91, 0x3c, 0x45,
-	0xf2, 0x04, 0x7b, 0x9f, 0x92, 0xd4, 0x0d, 0x8d, 0x8d, 0x27, 0xb9, 0x72, 0xeb, 0x3b, 0x9f, 0x8e,
-	0xa4, 0x73, 0x8e, 0x3e, 0x1d, 0x0c, 0xbb, 0x61, 0x30, 0x13, 0x3c, 0x0c, 0xa7, 0xc3, 0x13, 0xfd,
-	0x75, 0x3c, 0x0d, 0x03, 0x11, 0xa0, 0xe2, 0x1c, 0xaf, 0x16, 0xc3, 0xe9, 0x50, 0xa3, 0xb5, 0x3f,
-	0x67, 0x01, 0xf5, 0xb8, 0x3f, 0xba, 0x60, 0x77, 0x13, 0xee, 0x0b, 0xc2, 0x7f, 0x9e, 0xf1, 0x48,
-	0x20, 0x04, 0xd9, 0x11, 0x8f, 0x84, 0x65, 0x1c, 0x1a, 0x47, 0x65, 0xa2, 0xbe, 0x91, 0x09, 0x19,
-	0x36, 0x11, 0xd6, 0xda, 0xa1, 0x71, 0x94, 0x21, 0xf2, 0x13, 0x7d, 0x01, 0xe5, 0xa9, 0x9e, 0x47,
-	0xc7, 0x2c, 0x1a, 0x5b, 0x19, 0xc5, 0x2e, 0xc5, 0xd8, 0x39, 0x8b, 0xc6, 0xe8, 0x08, 0xcc, 0x2b,
-	0xd7, 0x67, 0x1e, 0x1d, 0x7a, 0xe2, 0x96, 0x8e, 0xb8, 0x27, 0x98, 0x95, 0x3d, 0x34, 0x8e, 0x72,
-	0x64, 0x43, 0xe1, 0x0d, 0x4f, 0xdc, 0x36, 0x25, 0x8a, 0xbe, 0x86, 0xcd, 0xc4, 0x59, 0xa8, 0x77,
-	0x61, 0xe5, 0x0e, 0x8d, 0xa3, 0x22, 0xd9, 0x98, 0x2e, 0xef, 0xed, 0x6b, 0xd8, 0x14, 0xee, 0x84,
-	0x07, 0x33, 0x41, 0x23, 0x3e, 0x0c, 0xfc, 0x51, 0x64, 0xad, 0x6b, 0x8f, 0x31, 0xdc, 0xd3, 0x28,
-	0xaa, 0x41, 0xe5, 0x8a, 0x73, 0xea, 0xb9, 0x13, 0x57, 0xd0, 0x88, 0x09, 0x2b, 0xaf, 0xb6, 0x5e,
-	0xba, 0xe2, 0xbc, 0x2d, 0xb1, 0x1e, 0x13, 0xe8, 0x15, 0x98, 0xc1, 0x4c, 0x5c, 0x07, 0xae, 0x7f,
-	0x4d, 0x87, 0x63, 0xe6, 0x53, 0x77, 0x64, 0x15, 0x0e, 0x8d, 0xa3, 0xec, 0xd9, 0xda, 0x6b, 0x83,
-	0x6c, 0x24, 0xb6, 0xc6, 0x98, 0xf9, 0xf6, 0x08, 0x1d, 0x00, 0xa8, 0x73, 0x28, 0x97, 0x56, 0x51,
-	0xad, 0x5a, 0x94, 0x88, 0xf2, 0x87, 0x4e, 0xa1, 0xa4, 0x82, 0x4c, 0xc7, 0xae, 0x2f, 0x22, 0x0b,
-	0x0e, 0x33, 0x47, 0xa5, 0x53, 0xf3, 0xd8, 0xf3, 0x65, 0xbc, 0x89, 0xb4, 0x9c, 0xbb, 0xbe, 0x20,
-	0x69, 0x12, 0xc2, 0x50, 0x90, 0xd1, 0xa5, 0xc2, 0xbb, 0xb5, 0x4a, 0x6a, 0xc2, 0xcb, 0xe3, 0x79,
-	0xa6, 0x8e, 0x1f, 0xa6, 0xe6, 0xb8, 0xc9, 0x23, 0xd1, 0xf7, 0x6e, 0xb1, 0x2f, 0xc2, 0x3b, 0x92,
-	0x1f, 0xe9, 0x51, 0xf5, 0x3b, 0x28, 0xa7, 0x0d, 0x32, 0x59, 0x37, 0xfc, 0x4e, 0xe5, 0x2f, 0x4b,
-	0xe4, 0x27, 0xda, 0x81, 0xdc, 0x2d, 0xf3, 0x66, 0x5c, 0x25, 0xb0, 0x4c, 0xf4, 0xe0, 0xbb, 0xb5,
-	0x37, 0x46, 0xed, 0x0d, 0x6c, 0xf7, 0x43, 0x36, 0xbc, 0xb9, 0x57, 0x03, 0xf7, 0xb3, 0x6b, 0x3c,
-	0xc8, 0x6e, 0xed, 0x4f, 0x50, 0x89, 0x27, 0xf5, 0x04, 0x13, 0xb3, 0x08, 0xfd, 0x3f, 0xe4, 0x22,
-	0xc1, 0x04, 0x57, 0xe4, 0x8d, 0xd3, 0xa7, 0xa9, 0xa3, 0xa4, 0x88, 0x9c, 0x68, 0x16, 0xaa, 0x42,
-	0x61, 0x1a, 0x72, 0x77, 0xc2, 0xae, 0x93, 0x6d, 0xcd, 0xc7, 0xa8, 0x06, 0x39, 0x35, 0x59, 0x55,
-	0x55, 0xe9, 0xb4, 0x9c, 0x0e, 0x23, 0xd1, 0xa6, 0xda, 0x6f, 0x60, 0x53, 0x8d, 0x5b, 0x9c, 0x7f,
-	0xaa, 0x72, 0x9f, 0x42, 0x9e, 0x4d, 0x74, 0x09, 0xe8, 0xea, 0x5d, 0x67, 0x13, 0x99, 0xfd, 0xda,
-	0x08, 0xcc, 0xc5, 0xfc, 0x68, 0x1a, 0xf8, 0x11, 0x97, 0x15, 0x2b, 0x9d, 0xcb, 0x82, 0x90, 0xd5,
-	0x33, 0x91, 0xb3, 0x0c, 0x35, 0x6b, 0x23, 0xc6, 0x5b, 0x9c, 0x77, 0x22, 0x26, 0xd0, 0x0b, 0x5d,
-	0x88, 0xd4, 0x0b, 0x86, 0x37, 0xb2, 0xb4, 0xd9, 0x5d, 0xec, 0xbe, 0x22, 0xe1, 0x76, 0x30, 0xbc,
-	0x69, 0x4a, 0xb0, 0xf6, 0x7b, 0x7d, 0xc5, 0xfa, 0x81, 0xde, 0xfb, 0xff, 0x1c, 0xde, 0x45, 0x08,
-	0xd6, 0x1e, 0x0f, 0x01, 0x85, 0xed, 0x25, 0xe7, 0xf1, 0x29, 0xd2, 0x91, 0x35, 0xee, 0x45, 0xf6,
-	0x15, 0xe4, 0xaf, 0x98, 0xeb, 0xcd, 0xc2, 0xc4, 0x31, 0x4a, 0xa5, 0xa9, 0xa5, 0x2d, 0x24, 0xa1,
-	0xd4, 0x7e, 0xc9, 0x43, 0x3e, 0x06, 0xd1, 0x29, 0x64, 0x87, 0xc1, 0x28, 0xc9, 0xee, 0xe7, 0x0f,
-	0xa7, 0x25, 0x7f, 0x1b, 0xc1, 0x88, 0x13, 0xc5, 0x45, 0xbf, 0x85, 0x0d, 0x79, 0xb1, 0x7c, 0xee,
-	0xd1, 0xd9, 0x74, 0xc4, 0xe6, 0x09, 0xb5, 0x52, 0xb3, 0x1b, 0x9a, 0x30, 0x50, 0x76, 0x52, 0x19,
-	0xa6, 0x87, 0x68, 0x0f, 0x8a, 0x63, 0xe1, 0x0d, 0x75, 0x26, 0xb2, 0xaa, 0xa0, 0x0b, 0x12, 0x50,
-	0x39, 0xa8, 0x41, 0x25, 0xf0, 0xdd, 0xc0, 0xa7, 0xd1, 0x98, 0xd1, 0xd3, 0x6f, 0xbe, 0x55, 0x9a,
-	0x51, 0x26, 0x25, 0x05, 0xf6, 0xc6, 0xec, 0xf4, 0x9b, 0x6f, 0xd1, 0x73, 0x28, 0xa9, 0x5b, 0xcb,
-	0x3f, 0x4e, 0xdd, 0xf0, 0x4e, 0x89, 0x45, 0x85, 0xa8, 0x8b, 0x8c, 0x15, 0x22, 0xaf, 0xc6, 0x95,
-	0xc7, 0xae, 0x23, 0x25, 0x10, 0x15, 0xa2, 0x07, 0xe8, 0x35, 0xec, 0xc4, 0x31, 0xa0, 0x51, 0x30,
-	0x0b, 0x87, 0x9c, 0xba, 0xfe, 0x88, 0x7f, 0x54, 0xf2, 0x50, 0x21, 0x28, 0xb6, 0xf5, 0x94, 0xc9,
-	0x96, 0x16, 0xb4, 0x0b, 0xeb, 0x63, 0xee, 0x5e, 0x8f, 0xb5, 0x34, 0x54, 0x48, 0x3c, 0xaa, 0xfd,
-	0x3d, 0x07, 0xa5, 0x54, 0x60, 0x50, 0x19, 0x0a, 0x04, 0xf7, 0x30, 0x79, 0x8f, 0x9b, 0xe6, 0x67,
-	0xe8, 0x08, 0xbe, 0xb2, 0x9d, 0x46, 0x97, 0x10, 0xdc, 0xe8, 0xd3, 0x2e, 0xa1, 0x03, 0xe7, 0x9d,
-	0xd3, 0xfd, 0xd1, 0xa1, 0x17, 0xf5, 0x0f, 0x1d, 0xec, 0xf4, 0x69, 0x13, 0xf7, 0xeb, 0x76, 0xbb,
-	0x67, 0x1a, 0x68, 0x1f, 0xac, 0x05, 0x33, 0x31, 0xd7, 0x3b, 0xdd, 0x81, 0xd3, 0x37, 0xd7, 0xd0,
-	0x73, 0xd8, 0x6b, 0xd9, 0x4e, 0xbd, 0x4d, 0x17, 0x9c, 0x46, 0xbb, 0xff, 0x9e, 0xe2, 0x9f, 0x2e,
-	0x6c, 0xf2, 0xc1, 0xcc, 0xac, 0x22, 0x9c, 0xf7, 0xdb, 0x8d, 0xc4, 0x43, 0x16, 0x3d, 0x83, 0x27,
-	0x9a, 0xa0, 0xa7, 0xd0, 0x7e, 0xb7, 0x4b, 0x7b, 0xdd, 0xae, 0x63, 0xe6, 0xd0, 0x16, 0x54, 0x6c,
-	0xe7, 0x7d, 0xbd, 0x6d, 0x37, 0x29, 0xc1, 0xf5, 0x76, 0xc7, 0x5c, 0x47, 0xdb, 0xb0, 0x79, 0x9f,
-	0x97, 0x97, 0x2e, 0x12, 0x5e, 0xd7, 0xb1, 0xbb, 0x0e, 0x7d, 0x8f, 0x49, 0xcf, 0xee, 0x3a, 0x66,
-	0x01, 0xed, 0x02, 0x5a, 0x36, 0x9d, 0x77, 0xea, 0x0d, 0xb3, 0x88, 0x9e, 0xc0, 0xd6, 0x32, 0xfe,
-	0x0e, 0x7f, 0x30, 0x01, 0x59, 0xb0, 0xa3, 0x37, 0x46, 0xcf, 0x70, 0xbb, 0xfb, 0x23, 0xed, 0xd8,
-	0x8e, 0xdd, 0x19, 0x74, 0xcc, 0x12, 0xda, 0x01, 0xb3, 0x85, 0x31, 0xb5, 0x9d, 0xde, 0xa0, 0xd5,
-	0xb2, 0x1b, 0x36, 0x76, 0xfa, 0x66, 0x59, 0xaf, 0xbc, 0xea, 0xe0, 0x15, 0x39, 0xa1, 0x71, 0x5e,
-	0x77, 0x1c, 0xdc, 0xa6, 0x4d, 0xbb, 0x57, 0x3f, 0x6b, 0xe3, 0xa6, 0xb9, 0x81, 0x0e, 0xe0, 0x59,
-	0x1f, 0x77, 0x2e, 0xba, 0xa4, 0x4e, 0x3e, 0xd0, 0xc4, 0xde, 0xaa, 0xdb, 0xed, 0x01, 0xc1, 0xe6,
-	0x26, 0xfa, 0x02, 0x0e, 0x08, 0xfe, 0x61, 0x60, 0x13, 0xdc, 0xa4, 0x4e, 0xb7, 0x89, 0x69, 0x0b,
-	0xd7, 0xfb, 0x03, 0x82, 0x69, 0xc7, 0xee, 0xf5, 0x6c, 0xe7, 0x7b, 0xd3, 0x44, 0x5f, 0xc1, 0xe1,
-	0x9c, 0x32, 0x77, 0x70, 0x8f, 0xb5, 0x25, 0xcf, 0x97, 0xa4, 0xd4, 0xc1, 0x3f, 0xf5, 0xe9, 0x05,
-	0xc6, 0xc4, 0x44, 0xa8, 0x0a, 0xbb, 0x8b, 0xe5, 0xf5, 0x02, 0xf1, 0xda, 0xdb, 0xd2, 0x76, 0x81,
-	0x49, 0xa7, 0xee, 0xc8, 0x04, 0x2f, 0xd9, 0x76, 0xe4, 0xb6, 0x17, 0xb6, 0xfb, 0xdb, 0x7e, 0x82,
-	0x10, 0x6c, 0xa4, 0xb2, 0xd2, 0xaa, 0x13, 0x73, 0x17, 0xed, 0xc0, 0x66, 0xb2, 0x83, 0x84, 0xf8,
-	0xaf, 0x3c, 0x7a, 0x0a, 0x68, 0xe0, 0x10, 0x5c, 0x6f, 0xca, 0x80, 0xcc, 0x0d, 0xff, 0xce, 0xbf,
-	0xcd, 0x16, 0xd6, 0xcc, 0x4c, 0xed, 0x1f, 0x19, 0xa8, 0x2c, 0xdd, 0x4b, 0xb4, 0x0f, 0xc5, 0xc8,
-	0xbd, 0xf6, 0x99, 0x90, 0xca, 0xa1, 0x45, 0x65, 0x01, 0xa8, 0xb7, 0x71, 0xcc, 0x5c, 0x5f, 0xab,
-	0x99, 0x56, 0xf3, 0xa2, 0x42, 0x94, 0x96, 0xed, 0x41, 0x3e, 0x79, 0x5f, 0x33, 0xf3, 0xf7, 0x75,
-	0x7d, 0xa8, 0xdf, 0xd5, 0x7d, 0x28, 0x4a, 0xc9, 0x8c, 0x04, 0x9b, 0x4c, 0xd5, 0x15, 0xaf, 0x90,
-	0x05, 0x80, 0xbe, 0x84, 0xca, 0x84, 0x47, 0x11, 0xbb, 0xe6, 0x54, 0x5f, 0x53, 0x50, 0x8c, 0x72,
-	0x0c, 0xb6, 0xd4, 0x6d, 0xfd, 0x12, 0x12, 0xd9, 0x88, 0x49, 0x39, 0x4d, 0x8a, 0x41, 0x4d, 0xba,
-	0xaf, 0xd8, 0x82, 0xc5, 0x6a, 0x90, 0x56, 0x6c, 0xc1, 0xd0, 0x4b, 0xd8, 0xd2, 0x92, 0xe3, 0xfa,
-	0xee, 0x64, 0x36, 0xd1, 0xd2, 0x93, 0x57, 0xd2, 0xb3, 0xa9, 0xa4, 0x47, 0xe3, 0x4a, 0x81, 0x9e,
-	0x41, 0xe1, 0x92, 0x45, 0x5c, 0x3e, 0x16, 0xb1, 0x34, 0xe4, 0xe5, 0xb8, 0xc5, 0xb9, 0x34, 0xc9,
-	0x27, 0x24, 0x94, 0xa2, 0xa7, 0x15, 0x21, 0x7f, 0xc5, 0x39, 0x91, 0xb1, 0x9c, 0xaf, 0xc0, 0x3e,
-	0x2e, 0x56, 0x28, 0xa5, 0x56, 0xd0, 0xb8, 0x5a, 0xe1, 0x25, 0x6c, 0xf1, 0x8f, 0x22, 0x64, 0x34,
-	0x98, 0xb2, 0x9f, 0x67, 0x9c, 0x8e, 0x98, 0x60, 0x56, 0x59, 0x05, 0x78, 0x53, 0x19, 0xba, 0x0a,
-	0x6f, 0x32, 0xc1, 0x6a, 0xfb, 0x50, 0x25, 0x3c, 0xe2, 0xa2, 0xe3, 0x46, 0x91, 0x1b, 0xf8, 0x8d,
-	0xc0, 0x17, 0x61, 0xe0, 0xc5, 0x6f, 0x4e, 0xed, 0x00, 0xf6, 0x56, 0x5a, 0xf5, 0xa3, 0x21, 0x27,
-	0xff, 0x30, 0xe3, 0xe1, 0xdd, 0xea, 0xc9, 0x77, 0xb0, 0xb7, 0xd2, 0x1a, 0xbf, 0x38, 0xaf, 0x20,
-	0xe7, 0x07, 0x23, 0x1e, 0x59, 0x86, 0xea, 0x62, 0x76, 0x53, 0xf2, 0xee, 0x04, 0x23, 0x7e, 0xee,
-	0x46, 0x22, 0x08, 0xef, 0x88, 0x26, 0x49, 0xf6, 0x94, 0xb9, 0x61, 0x64, 0xad, 0x3d, 0x60, 0x5f,
-	0x30, 0x37, 0x9c, 0xb3, 0x15, 0xa9, 0xf6, 0x17, 0x03, 0x4a, 0x29, 0x27, 0x52, 0x68, 0xa7, 0xb3,
-	0xcb, 0xa4, 0xc1, 0x29, 0x93, 0x78, 0x84, 0x5e, 0xc0, 0x86, 0xc7, 0x22, 0x41, 0xa5, 0x36, 0x53,
-	0x99, 0xd2, 0xf8, 0x41, 0xbe, 0x87, 0xa2, 0x63, 0x40, 0x81, 0x18, 0xf3, 0x90, 0x46, 0xb3, 0xe1,
-	0x90, 0x47, 0x11, 0x9d, 0x86, 0xc1, 0xa5, 0xaa, 0xcb, 0x35, 0xb2, 0xc2, 0xf2, 0x36, 0x5b, 0xc8,
-	0x9a, 0xb9, 0xda, 0x2f, 0x06, 0x94, 0x52, 0x9b, 0x93, 0x55, 0x2b, 0x0f, 0x43, 0xaf, 0xc2, 0x60,
-	0x92, 0xdc, 0x87, 0x39, 0x80, 0x2c, 0xc8, 0xab, 0x81, 0x08, 0xe2, 0xcb, 0x90, 0x0c, 0x97, 0xab,
-	0x3d, 0xa3, 0x36, 0x98, 0xaa, 0xf6, 0x53, 0xd8, 0x99, 0xb8, 0x3e, 0x9d, 0x72, 0x9f, 0x79, 0xee,
-	0x1f, 0x39, 0x4d, 0x3a, 0x97, 0xac, 0x22, 0xae, 0xb4, 0xa1, 0x1a, 0x94, 0x97, 0x4e, 0x92, 0x53,
-	0x27, 0x59, 0xc2, 0xd0, 0x1b, 0x78, 0xaa, 0xa2, 0xc0, 0x84, 0xe0, 0x93, 0xa9, 0x48, 0x0e, 0x78,
-	0x35, 0xf3, 0xd4, 0x1d, 0x28, 0x90, 0xc7, 0xcc, 0xb5, 0xbf, 0x19, 0xb0, 0x75, 0x36, 0x73, 0xbd,
-	0xd1, 0x52, 0xff, 0xf2, 0x0c, 0x0a, 0x72, 0xf9, 0x54, 0x7f, 0x24, 0x9b, 0x2c, 0x55, 0xb0, 0xab,
-	0x9a, 0xfe, 0xb5, 0x95, 0x4d, 0xff, 0xaa, 0xf6, 0x3b, 0xf3, 0x68, 0xfb, 0xfd, 0x1c, 0x4a, 0xe3,
-	0x60, 0x4a, 0x75, 0xb2, 0x23, 0x2b, 0x7b, 0x98, 0x39, 0x2a, 0x13, 0x18, 0x07, 0xd3, 0x0b, 0x8d,
-	0xd4, 0xde, 0x00, 0x4a, 0x6f, 0x34, 0xae, 0xcc, 0x79, 0x1b, 0x65, 0x3c, 0xda, 0x46, 0xbd, 0xfc,
-	0xab, 0x01, 0xe5, 0x74, 0x87, 0x8a, 0x2a, 0x50, 0xb4, 0x1d, 0xda, 0x6a, 0xdb, 0xdf, 0x9f, 0xf7,
-	0xcd, 0xcf, 0xe4, 0xb0, 0x37, 0x68, 0x34, 0x30, 0x6e, 0xe2, 0xa6, 0x69, 0x48, 0x95, 0x95, 0x82,
-	0x89, 0x9b, 0xb4, 0x6f, 0x77, 0x70, 0x77, 0x20, 0xdf, 0xdf, 0x6d, 0xd8, 0x8c, 0x31, 0xa7, 0x4b,
-	0x49, 0x77, 0xd0, 0xc7, 0x66, 0x06, 0x99, 0x50, 0x8e, 0x41, 0x4c, 0x48, 0x97, 0x98, 0x59, 0xf9,
-	0x68, 0xc4, 0xc8, 0xc3, 0xb7, 0x3c, 0x79, 0xea, 0x73, 0xa7, 0xff, 0xcc, 0xc2, 0xba, 0xda, 0x60,
-	0x88, 0xce, 0xa1, 0x94, 0xfa, 0x19, 0x80, 0x0e, 0x3e, 0xf9, 0xf3, 0xa0, 0x6a, 0xad, 0x6e, 0xb9,
-	0x67, 0xd1, 0x6b, 0x03, 0xbd, 0x85, 0x72, 0xba, 0xd1, 0x47, 0xe9, 0x06, 0x6e, 0xc5, 0x2f, 0x80,
-	0x4f, 0xfa, 0x7a, 0x07, 0x26, 0x8e, 0x84, 0x3b, 0x91, 0x0d, 0x5b, 0xdc, 0x42, 0xa3, 0x6a, 0x8a,
-	0x7f, 0xaf, 0x2f, 0xaf, 0xee, 0xad, 0xb4, 0xc5, 0x19, 0x6a, 0xeb, 0x23, 0xc6, 0x4d, 0xec, 0x83,
-	0x23, 0x2e, 0x77, 0xce, 0xd5, 0xcf, 0x1f, 0x33, 0xc7, 0xde, 0x46, 0xb0, 0xbd, 0x42, 0xe5, 0xd0,
-	0xff, 0xa5, 0x77, 0xf0, 0xa8, 0x46, 0x56, 0x5f, 0xfc, 0x37, 0xda, 0x62, 0x95, 0x15, 0x72, 0xb8,
-	0xb4, 0xca, 0xe3, 0x62, 0xba, 0xb4, 0xca, 0xa7, 0x54, 0xd5, 0x06, 0x58, 0x54, 0x34, 0xda, 0x4f,
-	0xcd, 0x7a, 0x70, 0x23, 0xab, 0x07, 0x8f, 0x58, 0xb5, 0xab, 0xb3, 0x5f, 0xfd, 0xee, 0xe4, 0xda,
-	0x15, 0xe3, 0xd9, 0xe5, 0xf1, 0x30, 0x98, 0x9c, 0x78, 0xb2, 0x33, 0xf5, 0x5d, 0xff, 0xda, 0xe7,
-	0xe2, 0x0f, 0x41, 0x78, 0x73, 0xe2, 0xf9, 0xa3, 0x13, 0x75, 0x31, 0x4e, 0xe6, 0x5e, 0x2e, 0xd7,
-	0xd5, 0x3f, 0x09, 0x7e, 0xfd, 0x9f, 0x00, 0x00, 0x00, 0xff, 0xff, 0xe1, 0xb1, 0x0f, 0x8c, 0x54,
-	0x10, 0x00, 0x00,
+	// 1805 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x57, 0x4f, 0x73, 0x1a, 0xc9,
+	0x15, 0xdf, 0x11, 0x20, 0xe0, 0x01, 0xd2, 0xa8, 0x25, 0xcb, 0x63, 0x64, 0xad, 0xb5, 0xec, 0x66,
+	0x57, 0xe5, 0x72, 0x24, 0x47, 0xa9, 0xdd, 0x72, 0xed, 0x21, 0x29, 0x0c, 0xcd, 0x6a, 0x6c, 0x98,
+	0x91, 0x1b, 0xf0, 0xae, 0x93, 0x43, 0x57, 0x1b, 0x5a, 0x62, 0x4a, 0xc3, 0x0c, 0x3b, 0xd3, 0x38,
+	0x56, 0x0e, 0xa9, 0xca, 0x07, 0xc8, 0xe7, 0xc8, 0x25, 0xb7, 0x7c, 0x91, 0xdc, 0xf2, 0x0d, 0x92,
+	0x4f, 0x90, 0x7b, 0xaa, 0xbb, 0x67, 0x60, 0x90, 0x90, 0x93, 0x93, 0xe8, 0xdf, 0xfb, 0xd7, 0xf3,
+	0xde, 0xeb, 0xdf, 0x7b, 0x82, 0xfd, 0x28, 0x9c, 0x0b, 0x1e, 0x45, 0xb3, 0xd1, 0xa9, 0xfe, 0x75,
+	0x32, 0x8b, 0x42, 0x11, 0xa2, 0xf2, 0x02, 0xaf, 0x97, 0xa3, 0xd9, 0x48, 0xa3, 0x8d, 0x3f, 0xe7,
+	0x01, 0xf5, 0x79, 0x30, 0xbe, 0x60, 0x37, 0x53, 0x1e, 0x08, 0xc2, 0x7f, 0x9e, 0xf3, 0x58, 0x20,
+	0x04, 0xf9, 0x31, 0x8f, 0x85, 0x65, 0x1c, 0x19, 0xc7, 0x55, 0xa2, 0x7e, 0x23, 0x13, 0x72, 0x6c,
+	0x2a, 0xac, 0x8d, 0x23, 0xe3, 0x38, 0x47, 0xe4, 0x4f, 0xf4, 0x05, 0x54, 0x67, 0xda, 0x8e, 0x4e,
+	0x58, 0x3c, 0xb1, 0x72, 0x4a, 0xbb, 0x92, 0x60, 0xe7, 0x2c, 0x9e, 0xa0, 0x63, 0x30, 0x2f, 0xbd,
+	0x80, 0xf9, 0x74, 0xe4, 0x8b, 0x0f, 0x74, 0xcc, 0x7d, 0xc1, 0xac, 0xfc, 0x91, 0x71, 0x5c, 0x20,
+	0x5b, 0x0a, 0x6f, 0xf9, 0xe2, 0x43, 0x5b, 0xa2, 0xe8, 0x1b, 0xd8, 0x4e, 0x9d, 0x45, 0xfa, 0x16,
+	0x56, 0xe1, 0xc8, 0x38, 0x2e, 0x93, 0xad, 0xd9, 0xea, 0xdd, 0xbe, 0x81, 0x6d, 0xe1, 0x4d, 0x79,
+	0x38, 0x17, 0x34, 0xe6, 0xa3, 0x30, 0x18, 0xc7, 0xd6, 0xa6, 0xf6, 0x98, 0xc0, 0x7d, 0x8d, 0xa2,
+	0x06, 0xd4, 0x2e, 0x39, 0xa7, 0xbe, 0x37, 0xf5, 0x04, 0x8d, 0x99, 0xb0, 0x8a, 0xea, 0xea, 0x95,
+	0x4b, 0xce, 0xbb, 0x12, 0xeb, 0x33, 0x81, 0x9e, 0x81, 0x19, 0xce, 0xc5, 0x55, 0xe8, 0x05, 0x57,
+	0x74, 0x34, 0x61, 0x01, 0xf5, 0xc6, 0x56, 0xe9, 0xc8, 0x38, 0xce, 0xbf, 0xdc, 0x78, 0x6e, 0x90,
+	0xad, 0x54, 0xd6, 0x9a, 0xb0, 0xc0, 0x1e, 0xa3, 0x43, 0x00, 0xf5, 0x1d, 0xca, 0xa5, 0x55, 0x56,
+	0x51, 0xcb, 0x12, 0x51, 0xfe, 0xd0, 0x19, 0x54, 0x54, 0x92, 0xe9, 0xc4, 0x0b, 0x44, 0x6c, 0xc1,
+	0x51, 0xee, 0xb8, 0x72, 0x66, 0x9e, 0xf8, 0x81, 0xcc, 0x37, 0x91, 0x92, 0x73, 0x2f, 0x10, 0x24,
+	0xab, 0x84, 0x30, 0x94, 0x64, 0x76, 0xa9, 0xf0, 0x3f, 0x58, 0x15, 0x65, 0xf0, 0xf4, 0x64, 0x51,
+	0xa9, 0x93, 0xbb, 0xa5, 0x39, 0x69, 0xf3, 0x58, 0x0c, 0xfc, 0x0f, 0x38, 0x10, 0xd1, 0x0d, 0x29,
+	0x8e, 0xf5, 0xa9, 0xfe, 0x3d, 0x54, 0xb3, 0x02, 0x59, 0xac, 0x6b, 0x7e, 0xa3, 0xea, 0x97, 0x27,
+	0xf2, 0x27, 0xda, 0x83, 0xc2, 0x07, 0xe6, 0xcf, 0xb9, 0x2a, 0x60, 0x95, 0xe8, 0xc3, 0xf7, 0x1b,
+	0x2f, 0x8c, 0xc6, 0x0b, 0xd8, 0x1d, 0x44, 0x6c, 0x74, 0x7d, 0xab, 0x07, 0x6e, 0x57, 0xd7, 0xb8,
+	0x53, 0xdd, 0xc6, 0x9f, 0xa0, 0x96, 0x18, 0xf5, 0x05, 0x13, 0xf3, 0x18, 0xfd, 0x12, 0x0a, 0xb1,
+	0x60, 0x82, 0x2b, 0xe5, 0xad, 0xb3, 0x87, 0x99, 0x4f, 0xc9, 0x28, 0x72, 0xa2, 0xb5, 0x50, 0x1d,
+	0x4a, 0xb3, 0x88, 0x7b, 0x53, 0x76, 0x95, 0x5e, 0x6b, 0x71, 0x46, 0x0d, 0x28, 0x28, 0x63, 0xd5,
+	0x55, 0x95, 0xb3, 0x6a, 0x36, 0x8d, 0x44, 0x8b, 0x1a, 0xbf, 0x81, 0x6d, 0x75, 0xee, 0x70, 0xfe,
+	0xa9, 0xce, 0x7d, 0x08, 0x45, 0x36, 0xd5, 0x2d, 0xa0, 0xbb, 0x77, 0x93, 0x4d, 0x65, 0xf5, 0x1b,
+	0x63, 0x30, 0x97, 0xf6, 0xf1, 0x2c, 0x0c, 0x62, 0x2e, 0x3b, 0x56, 0x3a, 0x97, 0x0d, 0x21, 0xbb,
+	0x67, 0x2a, 0xad, 0x0c, 0x65, 0xb5, 0x95, 0xe0, 0x1d, 0xce, 0x7b, 0x31, 0x13, 0xe8, 0x6b, 0xdd,
+	0x88, 0xd4, 0x0f, 0x47, 0xd7, 0xb2, 0xb5, 0xd9, 0x4d, 0xe2, 0xbe, 0x26, 0xe1, 0x6e, 0x38, 0xba,
+	0x6e, 0x4b, 0xb0, 0xf1, 0x7b, 0xfd, 0xc4, 0x06, 0xa1, 0xbe, 0xfb, 0xff, 0x9d, 0xde, 0x65, 0x0a,
+	0x36, 0xee, 0x4f, 0x01, 0x85, 0xdd, 0x15, 0xe7, 0xc9, 0x57, 0x64, 0x33, 0x6b, 0xdc, 0xca, 0xec,
+	0x33, 0x28, 0x5e, 0x32, 0xcf, 0x9f, 0x47, 0xa9, 0x63, 0x94, 0x29, 0x53, 0x47, 0x4b, 0x48, 0xaa,
+	0xd2, 0xf8, 0x4f, 0x11, 0x8a, 0x09, 0x88, 0xce, 0x20, 0x3f, 0x0a, 0xc7, 0x69, 0x75, 0x3f, 0xbf,
+	0x6b, 0x96, 0xfe, 0x6d, 0x85, 0x63, 0x4e, 0x94, 0x2e, 0xfa, 0x2d, 0x6c, 0xc9, 0x87, 0x15, 0x70,
+	0x9f, 0xce, 0x67, 0x63, 0xb6, 0x28, 0xa8, 0x95, 0xb1, 0x6e, 0x69, 0x85, 0xa1, 0x92, 0x93, 0xda,
+	0x28, 0x7b, 0x44, 0x07, 0x50, 0x9e, 0x08, 0x7f, 0xa4, 0x2b, 0x91, 0x57, 0x0d, 0x5d, 0x92, 0x80,
+	0xaa, 0x41, 0x03, 0x6a, 0x61, 0xe0, 0x85, 0x01, 0x8d, 0x27, 0x8c, 0x9e, 0x7d, 0xfb, 0x9d, 0xe2,
+	0x8c, 0x2a, 0xa9, 0x28, 0xb0, 0x3f, 0x61, 0x67, 0xdf, 0x7e, 0x87, 0x9e, 0x40, 0x45, 0xbd, 0x5a,
+	0xfe, 0x71, 0xe6, 0x45, 0x37, 0x8a, 0x2c, 0x6a, 0x44, 0x3d, 0x64, 0xac, 0x10, 0xf9, 0x34, 0x2e,
+	0x7d, 0x76, 0x15, 0x2b, 0x82, 0xa8, 0x11, 0x7d, 0x40, 0xcf, 0x61, 0x2f, 0xc9, 0x01, 0x8d, 0xc3,
+	0x79, 0x34, 0xe2, 0xd4, 0x0b, 0xc6, 0xfc, 0xa3, 0xa2, 0x87, 0x1a, 0x41, 0x89, 0xac, 0xaf, 0x44,
+	0xb6, 0x94, 0xa0, 0x7d, 0xd8, 0x9c, 0x70, 0xef, 0x6a, 0xa2, 0xa9, 0xa1, 0x46, 0x92, 0x53, 0xe3,
+	0x6f, 0x05, 0xa8, 0x64, 0x12, 0x83, 0xaa, 0x50, 0x22, 0xb8, 0x8f, 0xc9, 0x5b, 0xdc, 0x36, 0x3f,
+	0x43, 0xc7, 0xf0, 0x95, 0xed, 0xb4, 0x5c, 0x42, 0x70, 0x6b, 0x40, 0x5d, 0x42, 0x87, 0xce, 0x6b,
+	0xc7, 0xfd, 0xd1, 0xa1, 0x17, 0xcd, 0x77, 0x3d, 0xec, 0x0c, 0x68, 0x1b, 0x0f, 0x9a, 0x76, 0xb7,
+	0x6f, 0x1a, 0xe8, 0x31, 0x58, 0x4b, 0xcd, 0x54, 0xdc, 0xec, 0xb9, 0x43, 0x67, 0x60, 0x6e, 0xa0,
+	0x27, 0x70, 0xd0, 0xb1, 0x9d, 0x66, 0x97, 0x2e, 0x75, 0x5a, 0xdd, 0xc1, 0x5b, 0x8a, 0x7f, 0xba,
+	0xb0, 0xc9, 0x3b, 0x33, 0xb7, 0x4e, 0xe1, 0x7c, 0xd0, 0x6d, 0xa5, 0x1e, 0xf2, 0xe8, 0x11, 0x3c,
+	0xd0, 0x0a, 0xda, 0x84, 0x0e, 0x5c, 0x97, 0xf6, 0x5d, 0xd7, 0x31, 0x0b, 0x68, 0x07, 0x6a, 0xb6,
+	0xf3, 0xb6, 0xd9, 0xb5, 0xdb, 0x94, 0xe0, 0x66, 0xb7, 0x67, 0x6e, 0xa2, 0x5d, 0xd8, 0xbe, 0xad,
+	0x57, 0x94, 0x2e, 0x52, 0x3d, 0xd7, 0xb1, 0x5d, 0x87, 0xbe, 0xc5, 0xa4, 0x6f, 0xbb, 0x8e, 0x59,
+	0x42, 0xfb, 0x80, 0x56, 0x45, 0xe7, 0xbd, 0x66, 0xcb, 0x2c, 0xa3, 0x07, 0xb0, 0xb3, 0x8a, 0xbf,
+	0xc6, 0xef, 0x4c, 0x40, 0x16, 0xec, 0xe9, 0x8b, 0xd1, 0x97, 0xb8, 0xeb, 0xfe, 0x48, 0x7b, 0xb6,
+	0x63, 0xf7, 0x86, 0x3d, 0xb3, 0x82, 0xf6, 0xc0, 0xec, 0x60, 0x4c, 0x6d, 0xa7, 0x3f, 0xec, 0x74,
+	0xec, 0x96, 0x8d, 0x9d, 0x81, 0x59, 0xd5, 0x91, 0xd7, 0x7d, 0x78, 0x4d, 0x1a, 0xb4, 0xce, 0x9b,
+	0x8e, 0x83, 0xbb, 0xb4, 0x6d, 0xf7, 0x9b, 0x2f, 0xbb, 0xb8, 0x6d, 0x6e, 0xa1, 0x43, 0x78, 0x34,
+	0xc0, 0xbd, 0x0b, 0x97, 0x34, 0xc9, 0x3b, 0x9a, 0xca, 0x3b, 0x4d, 0xbb, 0x3b, 0x24, 0xd8, 0xdc,
+	0x46, 0x5f, 0xc0, 0x21, 0xc1, 0x6f, 0x86, 0x36, 0xc1, 0x6d, 0xea, 0xb8, 0x6d, 0x4c, 0x3b, 0xb8,
+	0x39, 0x18, 0x12, 0x4c, 0x7b, 0x76, 0xbf, 0x6f, 0x3b, 0x3f, 0x98, 0x26, 0xfa, 0x0a, 0x8e, 0x16,
+	0x2a, 0x0b, 0x07, 0xb7, 0xb4, 0x76, 0xe4, 0xf7, 0xa5, 0x25, 0x75, 0xf0, 0x4f, 0x03, 0x7a, 0x81,
+	0x31, 0x31, 0x11, 0xaa, 0xc3, 0xfe, 0x32, 0xbc, 0x0e, 0x90, 0xc4, 0xde, 0x95, 0xb2, 0x0b, 0x4c,
+	0x7a, 0x4d, 0x47, 0x16, 0x78, 0x45, 0xb6, 0x27, 0xaf, 0xbd, 0x94, 0xdd, 0xbe, 0xf6, 0x03, 0x84,
+	0x60, 0x2b, 0x53, 0x95, 0x4e, 0x93, 0x98, 0xfb, 0x68, 0x0f, 0xb6, 0xd3, 0x1b, 0xa4, 0x8a, 0xff,
+	0x2a, 0xa2, 0x87, 0x80, 0x86, 0x0e, 0xc1, 0xcd, 0xb6, 0x4c, 0xc8, 0x42, 0xf0, 0xef, 0xe2, 0xab,
+	0x7c, 0x69, 0xc3, 0xcc, 0x35, 0xfe, 0x9e, 0x83, 0xda, 0xca, 0xbb, 0x44, 0x8f, 0xa1, 0x1c, 0x7b,
+	0x57, 0x01, 0x13, 0x92, 0x39, 0x34, 0xa9, 0x2c, 0x01, 0x35, 0x1b, 0x27, 0xcc, 0x0b, 0x34, 0x9b,
+	0x69, 0x36, 0x2f, 0x2b, 0x44, 0x71, 0xd9, 0x01, 0x14, 0xd3, 0xf9, 0x9a, 0x5b, 0xcc, 0xd7, 0xcd,
+	0x91, 0x9e, 0xab, 0x8f, 0xa1, 0x2c, 0x29, 0x33, 0x16, 0x6c, 0x3a, 0x53, 0x4f, 0xbc, 0x46, 0x96,
+	0x00, 0xfa, 0x12, 0x6a, 0x53, 0x1e, 0xc7, 0xec, 0x8a, 0x53, 0xfd, 0x4c, 0x41, 0x69, 0x54, 0x13,
+	0xb0, 0xa3, 0x5e, 0xeb, 0x97, 0x90, 0xd2, 0x46, 0xa2, 0x54, 0xd0, 0x4a, 0x09, 0xa8, 0x95, 0x6e,
+	0x33, 0xb6, 0x60, 0x09, 0x1b, 0x64, 0x19, 0x5b, 0x30, 0xf4, 0x14, 0x76, 0x34, 0xe5, 0x78, 0x81,
+	0x37, 0x9d, 0x4f, 0x35, 0xf5, 0x14, 0x15, 0xf5, 0x6c, 0x2b, 0xea, 0xd1, 0xb8, 0x62, 0xa0, 0x47,
+	0x50, 0x7a, 0xcf, 0x62, 0x2e, 0x87, 0x45, 0x42, 0x0d, 0x45, 0x79, 0xee, 0x70, 0x2e, 0x45, 0x72,
+	0x84, 0x44, 0x92, 0xf4, 0x34, 0x23, 0x14, 0x2f, 0x39, 0x27, 0x32, 0x97, 0x8b, 0x08, 0xec, 0xe3,
+	0x32, 0x42, 0x25, 0x13, 0x41, 0xe3, 0x2a, 0xc2, 0x53, 0xd8, 0xe1, 0x1f, 0x45, 0xc4, 0x68, 0x38,
+	0x63, 0x3f, 0xcf, 0x39, 0x1d, 0x33, 0xc1, 0xac, 0xaa, 0x4a, 0xf0, 0xb6, 0x12, 0xb8, 0x0a, 0x6f,
+	0x33, 0xc1, 0x1a, 0x8f, 0xa1, 0x4e, 0x78, 0xcc, 0x45, 0xcf, 0x8b, 0x63, 0x2f, 0x0c, 0x5a, 0x61,
+	0x20, 0xa2, 0xd0, 0x4f, 0x66, 0x4e, 0xe3, 0x10, 0x0e, 0xd6, 0x4a, 0xf5, 0xd0, 0x90, 0xc6, 0x6f,
+	0xe6, 0x3c, 0xba, 0x59, 0x6f, 0xfc, 0x06, 0x0e, 0xd6, 0x4a, 0x93, 0x89, 0xf3, 0x0c, 0x0a, 0x33,
+	0xe6, 0x45, 0xb1, 0xb5, 0xa1, 0xb6, 0x98, 0xfd, 0x95, 0xd1, 0xef, 0x45, 0xe7, 0x5e, 0x2c, 0xc2,
+	0xe8, 0x86, 0x68, 0xa5, 0x57, 0xf9, 0x92, 0x61, 0x6e, 0x34, 0xfe, 0x69, 0x40, 0x25, 0x23, 0x94,
+	0x7d, 0x10, 0x84, 0x63, 0x4e, 0x2f, 0xa3, 0x70, 0x9a, 0x76, 0xd8, 0x02, 0x40, 0x16, 0x14, 0xd5,
+	0x41, 0x84, 0x49, 0x7b, 0xa5, 0xc7, 0xd5, 0xfe, 0xc9, 0xa9, 0x19, 0x9c, 0xe9, 0x9f, 0x33, 0xd8,
+	0x9b, 0x7a, 0x01, 0x9d, 0xf1, 0x80, 0xf9, 0xde, 0x1f, 0x39, 0x4d, 0x77, 0x81, 0xbc, 0x52, 0x5c,
+	0x2b, 0x43, 0x2f, 0xe0, 0xa1, 0xcf, 0x62, 0x41, 0x99, 0x10, 0x7c, 0x3a, 0x13, 0x34, 0x9e, 0x8f,
+	0x46, 0x3c, 0x8e, 0x2f, 0xe7, 0xbe, 0xea, 0x98, 0x12, 0xb9, 0x4f, 0xfc, 0x2a, 0x5f, 0x2a, 0x98,
+	0x9b, 0x8d, 0xbf, 0x1a, 0xb0, 0xf3, 0x72, 0xee, 0xf9, 0xe3, 0x95, 0x99, 0xff, 0x08, 0x4a, 0x32,
+	0x40, 0x66, 0xa7, 0x90, 0x8b, 0x89, 0x2a, 0xf2, 0xba, 0x45, 0x79, 0x63, 0xed, 0xa2, 0xbc, 0x6e,
+	0x65, 0xcd, 0xdd, 0xbb, 0xb2, 0x3e, 0x81, 0xca, 0x24, 0x9c, 0xd1, 0xd9, 0xfc, 0xfd, 0x35, 0xbf,
+	0x89, 0xad, 0xfc, 0x51, 0xee, 0xb8, 0x4a, 0x60, 0x12, 0xce, 0x2e, 0x34, 0xd2, 0x78, 0x01, 0x28,
+	0x7b, 0xd1, 0xa4, 0x9a, 0x8b, 0xd5, 0xc3, 0xb8, 0x77, 0xf5, 0x78, 0xfa, 0x17, 0x03, 0xaa, 0xd9,
+	0xad, 0x0e, 0xd5, 0xa0, 0x6c, 0x3b, 0xb4, 0xd3, 0xb5, 0x7f, 0x38, 0x1f, 0x98, 0x9f, 0xc9, 0x63,
+	0x7f, 0xd8, 0x6a, 0x61, 0xdc, 0xc6, 0x6d, 0xd3, 0x90, 0xcc, 0x24, 0x49, 0x06, 0xb7, 0xe9, 0xc0,
+	0xee, 0x61, 0x77, 0x28, 0x67, 0xd6, 0x2e, 0x6c, 0x27, 0x98, 0xe3, 0x52, 0xe2, 0x0e, 0x07, 0xd8,
+	0xcc, 0x21, 0x13, 0xaa, 0x09, 0x88, 0x09, 0x71, 0x89, 0x99, 0x97, 0x44, 0x9b, 0x20, 0x77, 0xe7,
+	0x5f, 0x3a, 0x1e, 0x0b, 0x67, 0xff, 0xc8, 0xc3, 0xa6, 0xba, 0x60, 0x84, 0xce, 0xa1, 0x92, 0x59,
+	0x9d, 0xd1, 0xe1, 0x27, 0x57, 0xea, 0xba, 0xb5, 0x7e, 0x4d, 0x9d, 0xc7, 0xcf, 0x0d, 0xf4, 0x0a,
+	0xaa, 0xd9, 0xe5, 0x18, 0x65, 0x97, 0x9e, 0x35, 0x5b, 0xf3, 0x27, 0x7d, 0xbd, 0x06, 0x13, 0xc7,
+	0xc2, 0x9b, 0xca, 0x25, 0x27, 0x59, 0x3b, 0x51, 0x3d, 0xa3, 0x7f, 0x6b, 0x97, 0xad, 0x1f, 0xac,
+	0x95, 0x25, 0x15, 0xea, 0xea, 0x4f, 0x4c, 0x16, 0xbf, 0x3b, 0x9f, 0xb8, 0xba, 0x6d, 0xd6, 0x3f,
+	0xbf, 0x4f, 0x9c, 0x78, 0x1b, 0xc3, 0xee, 0x1a, 0x66, 0x40, 0xbf, 0xc8, 0xde, 0xe0, 0x5e, 0x5e,
+	0xa9, 0x7f, 0xfd, 0xbf, 0xd4, 0x96, 0x51, 0xd6, 0x50, 0xc8, 0x4a, 0x94, 0xfb, 0x09, 0x68, 0x25,
+	0xca, 0xa7, 0x98, 0xc8, 0x06, 0x58, 0x76, 0x34, 0x7a, 0x9c, 0xb1, 0xba, 0xf3, 0x22, 0xeb, 0x87,
+	0xf7, 0x48, 0xb5, 0xab, 0x97, 0xbf, 0xfa, 0xdd, 0xe9, 0x95, 0x27, 0x26, 0xf3, 0xf7, 0x27, 0xa3,
+	0x70, 0x7a, 0xea, 0xcb, 0x6d, 0x2e, 0xf0, 0x82, 0xab, 0x80, 0x8b, 0x3f, 0x84, 0xd1, 0xf5, 0xa9,
+	0x1f, 0x8c, 0x4f, 0xd5, 0xc3, 0x38, 0x5d, 0x78, 0x79, 0xbf, 0xa9, 0xfe, 0xb1, 0xfe, 0xf5, 0x7f,
+	0x03, 0x00, 0x00, 0xff, 0xff, 0x3e, 0x15, 0x37, 0x36, 0x88, 0x0f, 0x00, 0x00,
 }
 
 // Reference imports to suppress errors if they are not otherwise used.
diff --git a/lnrpc/routerrpc/router.proto b/lnrpc/routerrpc/router.proto
index 612a374f0..906f25421 100644
--- a/lnrpc/routerrpc/router.proto
+++ b/lnrpc/routerrpc/router.proto
@@ -337,30 +337,12 @@ message QueryMissionControlRequest {}
 
 /// QueryMissionControlResponse contains mission control state.
 message QueryMissionControlResponse {
-    /// Node-level mission control state.
-    repeated NodeHistory nodes = 1 [json_name = "nodes"];
+    reserved 1;
 
     /// Node pair-level mission control state.
     repeated PairHistory pairs = 2 [json_name = "pairs"];
 }
 
-/// NodeHistory contains the mission control state for a particular node.
-message NodeHistory {
-    /// Node pubkey
-    bytes pubkey = 1 [json_name = "pubkey"];
-
-    /// Time stamp of last failure. Set to zero if no failure happened yet.
-    int64 last_fail_time = 2 [json_name = "last_fail_time"];
-
-    /**
-    Estimation of success probability of forwarding towards peers of this node
-    for which no specific history is available.
-    **/
-    float other_success_prob = 3 [json_name = "other_success_prob"];
-
-    reserved 4;
-}
-
 /// PairHistory contains the mission control state for a particular node pair.
 message PairHistory {
     /// The source node pubkey of the pair.
@@ -375,8 +357,7 @@ message PairHistory {
     /// Minimum penalization amount (only applies to failed attempts).
     int64 min_penalize_amt_sat = 4 [json_name = "min_penalize_amt_sat"];
 
-    /// Estimation of success probability for this pair.
-    float success_prob = 5 [json_name = "success_prob"];
+    reserved 5;
 
     /// Whether the last payment attempt through this pair was successful.
     bool last_attempt_successful = 6 [json_name = "last_attempt_successful"];
diff --git a/lnrpc/routerrpc/router_server.go b/lnrpc/routerrpc/router_server.go
index 37e68474e..77a701bbc 100644
--- a/lnrpc/routerrpc/router_server.go
+++ b/lnrpc/routerrpc/router_server.go
@@ -466,22 +466,6 @@ func (s *Server) QueryMissionControl(ctx context.Context,
 
 	snapshot := s.cfg.RouterBackend.MissionControl.GetHistorySnapshot()
 
-	rpcNodes := make([]*NodeHistory, 0, len(snapshot.Nodes))
-	for _, n := range snapshot.Nodes {
-		// Copy node struct to prevent loop variable binding bugs.
-		node := n
-
-		rpcNode := NodeHistory{
-			Pubkey:       node.Node[:],
-			LastFailTime: node.LastFail.Unix(),
-			OtherSuccessProb: float32(
-				node.OtherSuccessProb,
-			),
-		}
-
-		rpcNodes = append(rpcNodes, &rpcNode)
-	}
-
 	rpcPairs := make([]*PairHistory, 0, len(snapshot.Pairs))
 	for _, p := range snapshot.Pairs {
 		// Prevent binding to loop variable.
@@ -494,7 +478,6 @@ func (s *Server) QueryMissionControl(ctx context.Context,
 			MinPenalizeAmtSat: int64(
 				pair.MinPenalizeAmt.ToSatoshis(),
 			),
-			SuccessProb:           float32(pair.SuccessProb),
 			LastAttemptSuccessful: pair.LastAttemptSuccessful,
 		}
 
@@ -502,7 +485,6 @@ func (s *Server) QueryMissionControl(ctx context.Context,
 	}
 
 	response := QueryMissionControlResponse{
-		Nodes: rpcNodes,
 		Pairs: rpcPairs,
 	}
 
diff --git a/routing/missioncontrol.go b/routing/missioncontrol.go
index cebb7ea98..de93df0da 100644
--- a/routing/missioncontrol.go
+++ b/routing/missioncontrol.go
@@ -1,7 +1,6 @@
 package routing
 
 import (
-	"math"
 	"sync"
 	"time"
 
@@ -47,8 +46,15 @@ const (
 	// prevSuccessProbability is the assumed probability for node pairs that
 	// successfully relayed the previous attempt.
 	prevSuccessProbability = 0.95
+
+	// DefaultAprioriWeight is the default a priori weight. See
+	// MissionControlConfig for further explanation.
+	DefaultAprioriWeight = 0.5
 )
 
+// NodeResults contains previous results from a node to its peers.
+type NodeResults map[route.Vertex]timedPairResult
+
 // MissionControl contains state which summarizes the past attempts of HTLC
 // routing by external callers when sending payments throughout the network. It
 // acts as a shared memory during routing attempts with the goal to optimize the
@@ -59,11 +65,11 @@ const (
 // since the last failure is used to estimate a success probability that is fed
 // into the path finding process for subsequent payment attempts.
 type MissionControl struct {
-	// lastPairResult tracks the last payment result per node pair.
-	lastPairResult map[DirectedNodePair]timedPairResult
-
-	// lastNodeFailure tracks the last node level failure per node.
-	lastNodeFailure map[route.Vertex]time.Time
+	// lastPairResult tracks the last payment result (on a pair basis) for
+	// each transited node. This is a multi-layer map that allows us to look
+	// up the failure history of all connected channels (node pairs) for a
+	// particular node.
+	lastPairResult map[route.Vertex]NodeResults
 
 	// lastSecondChance tracks the last time a second chance was granted for
 	// a directed node pair.
@@ -77,6 +83,10 @@ type MissionControl struct {
 
 	store *missionControlStore
 
+	// estimator is the probability estimator that is used with the payment
+	// results that mission control collects.
+	estimator *probabilityEstimator
+
 	sync.Mutex
 
 	// TODO(roasbeef): further counters, if vertex continually unavailable,
@@ -99,6 +109,15 @@ type MissionControlConfig struct {
 	// MaxMcHistory defines the maximum number of payment results that are
 	// held on disk.
 	MaxMcHistory int
+
+	// 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
 }
 
 // timedPairResult describes a timestamped pair result.
@@ -112,28 +131,11 @@ type timedPairResult struct {
 // MissionControlSnapshot contains a snapshot of the current state of mission
 // control.
 type MissionControlSnapshot struct {
-	// Nodes contains the per node information of this snapshot.
-	Nodes []MissionControlNodeSnapshot
-
 	// Pairs is a list of channels for which specific information is
 	// logged.
 	Pairs []MissionControlPairSnapshot
 }
 
-// MissionControlNodeSnapshot contains a snapshot of the current node state in
-// mission control.
-type MissionControlNodeSnapshot struct {
-	// Node pubkey.
-	Node route.Vertex
-
-	// LastFail is the time of last failure.
-	LastFail time.Time
-
-	// OtherSuccessProb is the success probability for pairs not in
-	// the Pairs slice.
-	OtherSuccessProb float64
-}
-
 // MissionControlPairSnapshot contains a snapshot of the current node pair
 // state in mission control.
 type MissionControlPairSnapshot struct {
@@ -147,9 +149,6 @@ type MissionControlPairSnapshot struct {
 	// penalized.
 	MinPenalizeAmt lnwire.MilliSatoshi
 
-	// SuccessProb is the success probability estimation for this channel.
-	SuccessProb float64
-
 	// LastAttemptSuccessful indicates whether the last payment attempt
 	// through this pair was successful.
 	LastAttemptSuccessful bool
@@ -171,21 +170,29 @@ func NewMissionControl(db *bbolt.DB, cfg *MissionControlConfig) (
 	*MissionControl, error) {
 
 	log.Debugf("Instantiating mission control with config: "+
-		"PenaltyHalfLife=%v, AprioriHopProbability=%v",
-		cfg.PenaltyHalfLife, cfg.AprioriHopProbability)
+		"PenaltyHalfLife=%v, AprioriHopProbability=%v, "+
+		"AprioriWeight=%v", cfg.PenaltyHalfLife,
+		cfg.AprioriHopProbability, cfg.AprioriWeight)
 
 	store, err := newMissionControlStore(db, cfg.MaxMcHistory)
 	if err != nil {
 		return nil, err
 	}
 
+	estimator := &probabilityEstimator{
+		aprioriHopProbability:  cfg.AprioriHopProbability,
+		aprioriWeight:          cfg.AprioriWeight,
+		penaltyHalfLife:        cfg.PenaltyHalfLife,
+		prevSuccessProbability: prevSuccessProbability,
+	}
+
 	mc := &MissionControl{
-		lastPairResult:   make(map[DirectedNodePair]timedPairResult),
-		lastNodeFailure:  make(map[route.Vertex]time.Time),
+		lastPairResult:   make(map[route.Vertex]NodeResults),
 		lastSecondChance: make(map[DirectedNodePair]time.Time),
 		now:              time.Now,
 		cfg:              cfg,
 		store:            store,
+		estimator:        estimator,
 	}
 
 	if err := mc.init(); err != nil {
@@ -226,8 +233,7 @@ func (m *MissionControl) ResetHistory() error {
 		return err
 	}
 
-	m.lastPairResult = make(map[DirectedNodePair]timedPairResult)
-	m.lastNodeFailure = make(map[route.Vertex]time.Time)
+	m.lastPairResult = make(map[route.Vertex]NodeResults)
 	m.lastSecondChance = make(map[DirectedNodePair]time.Time)
 
 	log.Debugf("Mission control history cleared")
@@ -243,62 +249,40 @@ func (m *MissionControl) GetProbability(fromNode, toNode route.Vertex,
 	m.Lock()
 	defer m.Unlock()
 
-	return m.getPairProbability(fromNode, toNode, amt)
-}
+	now := m.now()
+	results := m.lastPairResult[fromNode]
 
-// getProbAfterFail returns a probability estimate based on a last failure time.
-func (m *MissionControl) getProbAfterFail(lastFailure time.Time) float64 {
-	if lastFailure.IsZero() {
-		return m.cfg.AprioriHopProbability
-	}
+	return m.estimator.getPairProbability(now, results, toNode, amt)
+}
 
-	timeSinceLastFailure := m.now().Sub(lastFailure)
+// setLastPairResult stores a result for a node pair.
+func (m *MissionControl) setLastPairResult(fromNode,
+	toNode route.Vertex, result timedPairResult) {
 
-	// Calculate success probability. It is an exponential curve that brings
-	// the probability down to zero when a failure occurs. From there it
-	// recovers asymptotically back to the a priori probability. The rate at
-	// which this happens is controlled by the penaltyHalfLife parameter.
-	exp := -timeSinceLastFailure.Hours() / m.cfg.PenaltyHalfLife.Hours()
-	probability := m.cfg.AprioriHopProbability * (1 - math.Pow(2, exp))
+	nodePairs, ok := m.lastPairResult[fromNode]
+	if !ok {
+		nodePairs = make(NodeResults)
+		m.lastPairResult[fromNode] = nodePairs
+	}
 
-	return probability
+	nodePairs[toNode] = result
 }
 
-// getPairProbability estimates the probability of successfully
-// traversing from fromNode to toNode based on historical payment outcomes.
-func (m *MissionControl) getPairProbability(fromNode,
-	toNode route.Vertex, amt lnwire.MilliSatoshi) float64 {
-
-	// Start by getting the last node level failure. A node failure is
-	// considered a failure that would have affected every edge. Therefore
-	// we insert a node level failure into the history of every channel. If
-	// there is none, lastFail will be zero.
-	lastFail := m.lastNodeFailure[fromNode]
-
-	// Retrieve the last pair outcome.
-	pair := NewDirectedNodePair(fromNode, toNode)
-	lastPairResult, ok := m.lastPairResult[pair]
-
-	// Only look at the last pair outcome if it happened after the last node
-	// level failure. Otherwise the node level failure is the most recent
-	// and used as the basis for calculation of the probability.
-	if ok && lastPairResult.timestamp.After(lastFail) {
-		if lastPairResult.success {
-			return prevSuccessProbability
-		}
+// setAllFail stores a fail result for all known connection of the given node.
+func (m *MissionControl) setAllFail(fromNode route.Vertex,
+	timestamp time.Time) {
 
-		// Take into account a minimum penalize amount. For balance
-		// errors, a failure may be reported with such a minimum to
-		// prevent too aggresive penalization. We only take into account
-		// a previous failure if the amount that we currently get the
-		// probability for is greater or equal than the minPenalizeAmt
-		// of the previous failure.
-		if amt >= lastPairResult.minPenalizeAmt {
-			lastFail = lastPairResult.timestamp
-		}
+	nodePairs, ok := m.lastPairResult[fromNode]
+	if !ok {
+		return
 	}
 
-	return m.getProbAfterFail(lastFail)
+	for connection := range nodePairs {
+		nodePairs[connection] = timedPairResult{
+			timestamp:  timestamp,
+			pairResult: failPairResult(0),
+		}
+	}
 }
 
 // requestSecondChance checks whether the node fromNode can have a second chance
@@ -339,40 +323,27 @@ func (m *MissionControl) GetHistorySnapshot() *MissionControlSnapshot {
 	defer m.Unlock()
 
 	log.Debugf("Requesting history snapshot from mission control: "+
-		"node_failure_count=%v, pair_result_count=%v",
-		len(m.lastNodeFailure), len(m.lastPairResult))
+		"pair_result_count=%v", len(m.lastPairResult))
 
-	nodes := make([]MissionControlNodeSnapshot, 0, len(m.lastNodeFailure))
-	for v, h := range m.lastNodeFailure {
-		otherProb := m.getPairProbability(v, route.Vertex{}, 0)
+	pairs := make([]MissionControlPairSnapshot, 0, len(m.lastPairResult))
 
-		nodes = append(nodes, MissionControlNodeSnapshot{
-			Node:             v,
-			LastFail:         h,
-			OtherSuccessProb: otherProb,
-		})
-	}
+	for fromNode, fromPairs := range m.lastPairResult {
+		for toNode, result := range fromPairs {
 
-	pairs := make([]MissionControlPairSnapshot, 0, len(m.lastPairResult))
+			pair := NewDirectedNodePair(fromNode, toNode)
 
-	for v, h := range m.lastPairResult {
-		// Show probability assuming amount meets min
-		// penalization amount.
-		prob := m.getPairProbability(v.From, v.To, h.minPenalizeAmt)
-
-		pair := MissionControlPairSnapshot{
-			Pair:                  v,
-			MinPenalizeAmt:        h.minPenalizeAmt,
-			Timestamp:             h.timestamp,
-			SuccessProb:           prob,
-			LastAttemptSuccessful: h.success,
-		}
+			pairSnapshot := MissionControlPairSnapshot{
+				Pair:                  pair,
+				MinPenalizeAmt:        result.minPenalizeAmt,
+				Timestamp:             result.timestamp,
+				LastAttemptSuccessful: result.success,
+			}
 
-		pairs = append(pairs, pair)
+			pairs = append(pairs, pairSnapshot)
+		}
 	}
 
 	snapshot := MissionControlSnapshot{
-		Nodes: nodes,
 		Pairs: pairs,
 	}
 
@@ -463,11 +434,28 @@ func (m *MissionControl) applyPaymentResult(
 		}
 	}
 
+	// If there is a node-level failure, record a failure for every tried
+	// connection of that node. A node-level failure can be considered as a
+	// failure that would have occurred with any of the node's channels.
+	//
+	// Ideally we'd also record the failure for the untried connections of
+	// the node. Unfortunately this would require access to the graph and
+	// adding this dependency and db calls does not outweigh the benefits.
+	//
+	// Untried connections will fall back to the node probability. After the
+	// call to setAllPairResult below, the node probability will be equal to
+	// the probability of the tried channels except that the a priori
+	// probability is mixed in too. This effect is controlled by the
+	// aprioriWeight parameter. If that parameter isn't set to an extreme
+	// and there are a few known connections, there shouldn't be much of a
+	// difference. The largest difference occurs when aprioriWeight is 1. In
+	// that case, a node-level failure would not be applied to untried
+	// channels.
 	if i.nodeFailure != nil {
 		log.Debugf("Reporting node failure to Mission Control: "+
 			"node=%v", *i.nodeFailure)
 
-		m.lastNodeFailure[*i.nodeFailure] = result.timeReply
+		m.setAllFail(*i.nodeFailure, result.timeReply)
 	}
 
 	for pair, pairResult := range i.pairResults {
@@ -480,10 +468,10 @@ func (m *MissionControl) applyPaymentResult(
 				pair, pairResult.minPenalizeAmt)
 		}
 
-		m.lastPairResult[pair] = timedPairResult{
+		m.setLastPairResult(pair.From, pair.To, timedPairResult{
 			timestamp:  result.timeReply,
 			pairResult: pairResult,
-		}
+		})
 	}
 
 	return i.finalFailureReason
diff --git a/routing/probability_estimator.go b/routing/probability_estimator.go
new file mode 100644
index 000000000..a1303dfec
--- /dev/null
+++ b/routing/probability_estimator.go
@@ -0,0 +1,155 @@
+package routing
+
+import (
+	"math"
+	"time"
+
+	"github.com/lightningnetwork/lnd/lnwire"
+	"github.com/lightningnetwork/lnd/routing/route"
+)
+
+// probabilityEstimator returns node and pair probabilities based on historical
+// payment results.
+type probabilityEstimator 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
+
+	// 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) float64 {
+
+	// If the channel history is not to be taken into account, we can return
+	// early here with the configured a priori probability.
+	if p.aprioriWeight == 1 {
+		return p.aprioriHopProbability
+	}
+
+	// If there is no channel history, our best estimate is still the a
+	// priori probability.
+	if len(results) == 0 {
+		return p.aprioriHopProbability
+	}
+
+	// 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 := p.aprioriHopProbability * aprioriFactor
+	totalWeight := aprioriFactor
+
+	for _, result := range results {
+		age := now.Sub(result.timestamp)
+
+		switch {
+		// Weigh success with a constant high weight of 1. There is no
+		// decay.
+		case result.success:
+			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 amt >= result.minPenalizeAmt:
+			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)
+}
+
+// getPairProbability estimates the probability of successfully traversing to
+// toNode based on historical payment outcomes for the from node. Those outcomes
+// are passed in via the results parameter.
+func (p *probabilityEstimator) getPairProbability(
+	now time.Time, results NodeResults,
+	toNode route.Vertex, amt lnwire.MilliSatoshi) float64 {
+
+	// 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 p.getNodeProbability(now, results, amt)
+	}
+
+	// For successes, we have a fixed (high) probability. Those pairs
+	// will be assumed good until proven otherwise.
+	if lastPairResult.success {
+		return p.prevSuccessProbability
+	}
+
+	nodeProbability := p.getNodeProbability(now, results, amt)
+
+	// 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 amt < lastPairResult.minPenalizeAmt {
+		return nodeProbability
+	}
+
+	timeSinceLastFailure := now.Sub(lastPairResult.timestamp)
+
+	// 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/result_interpretation.go b/routing/result_interpretation.go
index 74c740149..696d930c6 100644
--- a/routing/result_interpretation.go
+++ b/routing/result_interpretation.go
@@ -26,6 +26,20 @@ type pairResult struct {
 	success bool
 }
 
+// failPairResult creates a new result struct for a failure.
+func failPairResult(minPenalizeAmt lnwire.MilliSatoshi) pairResult {
+	return pairResult{
+		minPenalizeAmt: minPenalizeAmt,
+	}
+}
+
+// successPairResult creates a new result struct for a success.
+func successPairResult() pairResult {
+	return pairResult{
+		success: true,
+	}
+}
+
 // String returns the human-readable representation of a pair result.
 func (p pairResult) String() string {
 	if p.success {
@@ -364,10 +378,30 @@ func (i *interpretedResult) processPaymentOutcomeUnknown(route *route.Route) {
 	i.failPairRange(route, 0, n-1)
 }
 
-// failNode marks the node indicated by idx in the route as failed. This
-// function intentionally panics when the self node is failed.
+// failNode marks the node indicated by idx in the route as failed. It also
+// marks the incoming and outgoing channels of the node as failed. This function
+// intentionally panics when the self node is failed.
 func (i *interpretedResult) failNode(rt *route.Route, idx int) {
+	// Mark the node as failing.
 	i.nodeFailure = &rt.Hops[idx-1].PubKeyBytes
+
+	// Mark the incoming connection as failed for the node. We intent to
+	// penalize as much as we can for a node level failure, including future
+	// outgoing traffic for this connection. The pair as it is returned by
+	// getPair is directed towards the failed node. Therefore we first
+	// reverse the pair. We don't want to affect the score of the node
+	// sending towards the failing node.
+	incomingChannelIdx := idx - 1
+	inPair, _ := getPair(rt, incomingChannelIdx)
+	i.pairResults[inPair.Reverse()] = failPairResult(0)
+
+	// If not the ultimate node, mark the outgoing connection as failed for
+	// the node.
+	if idx < len(rt.Hops) {
+		outgoingChannelIdx := idx
+		outPair, _ := getPair(rt, outgoingChannelIdx)
+		i.pairResults[outPair] = failPairResult(0)
+	}
 }
 
 // failPairRange marks the node pairs from node fromIdx to node toIdx as failed
@@ -387,8 +421,8 @@ func (i *interpretedResult) failPair(
 	pair, _ := getPair(rt, idx)
 
 	// Report pair in both directions without a minimum penalization amount.
-	i.pairResults[pair] = pairResult{}
-	i.pairResults[pair.Reverse()] = pairResult{}
+	i.pairResults[pair] = failPairResult(0)
+	i.pairResults[pair.Reverse()] = failPairResult(0)
 }
 
 // failPairBalance marks a pair as failed with a minimum penalization amount.
@@ -397,9 +431,7 @@ func (i *interpretedResult) failPairBalance(
 
 	pair, amt := getPair(rt, channelIdx)
 
-	i.pairResults[pair] = pairResult{
-		minPenalizeAmt: amt,
-	}
+	i.pairResults[pair] = failPairResult(amt)
 }
 
 // successPairRange marks the node pairs from node fromIdx to node toIdx as
@@ -410,9 +442,7 @@ func (i *interpretedResult) successPairRange(
 	for idx := fromIdx; idx <= toIdx; idx++ {
 		pair, _ := getPair(rt, idx)
 
-		i.pairResults[pair] = pairResult{
-			success: true,
-		}
+		i.pairResults[pair] = successPairResult()
 	}
 }
 
diff --git a/server.go b/server.go
index 5aa496622..08438bd79 100644
--- a/server.go
+++ b/server.go
@@ -660,6 +660,7 @@ func newServer(listenAddrs []net.Addr, chanDB *channeldb.DB,
 			AprioriHopProbability: routingConfig.AprioriHopProbability,
 			PenaltyHalfLife:       routingConfig.PenaltyHalfLife,
 			MaxMcHistory:          routingConfig.MaxMcHistory,
+			AprioriWeight:         routingConfig.AprioriWeight,
 		},
 	)
 	if err != nil {


Test patch:
diff --git a/routing/missioncontrol_test.go b/routing/missioncontrol_test.go
index df96e3140..bbef69c94 100644
--- a/routing/missioncontrol_test.go
+++ b/routing/missioncontrol_test.go
@@ -32,6 +32,10 @@ var (
 	mcTestTime  = time.Date(2018, time.January, 9, 14, 00, 00, 0, time.UTC)
 	mcTestNode1 = mcTestRoute.Hops[0].PubKeyBytes
 	mcTestNode2 = mcTestRoute.Hops[1].PubKeyBytes
+
+	testPenaltyHalfLife       = 30 * time.Minute
+	testAprioriHopProbability = 0.9
+	testAprioriWeight         = 0.5
 )
 
 type mcTestContext struct {
@@ -73,8 +77,9 @@ func (ctx *mcTestContext) restartMc() {
 	mc, err := NewMissionControl(
 		ctx.db,
 		&MissionControlConfig{
-			PenaltyHalfLife:       30 * time.Minute,
-			AprioriHopProbability: 0.8,
+			PenaltyHalfLife:       testPenaltyHalfLife,
+			AprioriHopProbability: testAprioriHopProbability,
+			AprioriWeight:         testAprioriWeight,
 		},
 	)
 	if err != nil {
@@ -133,20 +138,23 @@ func TestMissionControl(t *testing.T) {
 
 	testTime := time.Date(2018, time.January, 9, 14, 00, 00, 0, time.UTC)
 
-	// Initial probability is expected to be 1.
-	ctx.expectP(1000, 0.8)
+	// Initial probability is expected to be the a priori.
+	ctx.expectP(1000, testAprioriHopProbability)
 
 	// Expect probability to be zero after reporting the edge as failed.
 	ctx.reportFailure(1000, lnwire.NewTemporaryChannelFailure(nil))
 	ctx.expectP(1000, 0)
 
 	// As we reported with a min penalization amt, a lower amt than reported
-	// should be unaffected.
-	ctx.expectP(500, 0.8)
+	// should return the node probability, which is the a priori
+	// probability.
+	ctx.expectP(500, testAprioriHopProbability)
 
-	// Edge decay started.
+	// Edge decay started. The node probability weighted average should now
+	// have shifted from 1:1 to 1:0.5 -> 60%. The connection probability is
+	// half way through the recovery, so we expect 30% here.
 	ctx.now = testTime.Add(30 * time.Minute)
-	ctx.expectP(1000, 0.4)
+	ctx.expectP(1000, 0.3)
 
 	// Edge fails again, this time without a min penalization amt. The edge
 	// should be penalized regardless of amount.
@@ -156,26 +164,22 @@ func TestMissionControl(t *testing.T) {
 
 	// Edge decay started.
 	ctx.now = testTime.Add(60 * time.Minute)
-	ctx.expectP(1000, 0.4)
+	ctx.expectP(1000, 0.3)
 
 	// Restart mission control to test persistence.
 	ctx.restartMc()
-	ctx.expectP(1000, 0.4)
+	ctx.expectP(1000, 0.3)
 
-	// A node level failure should bring probability of every channel back
-	// to zero.
+	// A node level failure should bring probability of all known channels
+	// back to zero.
 	ctx.reportFailure(0, lnwire.NewExpiryTooSoon(lnwire.ChannelUpdate{}))
 	ctx.expectP(1000, 0)
 
 	// Check whether history snapshot looks sane.
 	history := ctx.mc.GetHistorySnapshot()
-	if len(history.Nodes) != 1 {
-		t.Fatalf("unexpected number of nodes: expected 1 got %v",
-			len(history.Nodes))
-	}
 
-	if len(history.Pairs) != 2 {
-		t.Fatalf("expected 2 pairs, but got %v", len(history.Pairs))
+	if len(history.Pairs) != 3 {
+		t.Fatalf("expected 3 pairs, but got %v", len(history.Pairs))
 	}
 
 	// Test reporting a success.
@@ -192,7 +196,7 @@ func TestMissionControlChannelUpdate(t *testing.T) {
 	ctx.reportFailure(
 		0, lnwire.NewFeeInsufficient(0, lnwire.ChannelUpdate{}),
 	)
-	ctx.expectP(0, 0.8)
+	ctx.expectP(0, testAprioriHopProbability)
 
 	// Report another failure for the same channel. We expect it to be
 	// pruned.
diff --git a/routing/probability_estimator_test.go b/routing/probability_estimator_test.go
new file mode 100644
index 000000000..c9604241d
--- /dev/null
+++ b/routing/probability_estimator_test.go
@@ -0,0 +1,163 @@
+package routing
+
+import (
+	"testing"
+	"time"
+
+	"github.com/lightningnetwork/lnd/lnwire"
+	"github.com/lightningnetwork/lnd/routing/route"
+)
+
+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
+)
+
+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
+}
+
+func newEstimatorTestContext(t *testing.T) *estimatorTestContext {
+	return &estimatorTestContext{
+		t: t,
+		estimator: &probabilityEstimator{
+			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, 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)
+	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)
+
+	ctx.assertPairProbability(testTime, 0, 0, aprioriHopProb)
+}
+
+// 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: {
+			timestamp:  testTime.Add(-time.Hour),
+			pairResult: successPairResult(),
+		},
+	}
+
+	// Because of the previous success, this channel keep reporting a high
+	// probability.
+	ctx.assertPairProbability(
+		testTime, node1, 100, aprioriPrevSucProb,
+	)
+
+	// Untried channels are also influenced by the success. With a
+	// aprioriWeight of 0.75, the a priori probability is assigned weight 3.
+	expectedP := (3*aprioriHopProb + 1*aprioriPrevSucProb) / 4
+	ctx.assertPairProbability(testTime, untriedNode, 100, expectedP)
+}
+
+// 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: {
+			timestamp:  testTime.Add(-time.Hour),
+			pairResult: failPairResult(0),
+		},
+	}
+
+	// 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, expectedNodeProb)
+
+	// The pair probability decays back to the node probability. With the
+	// weight at 0.5, we expected a pair probability of 0.5 * 0.51 = 0.25.
+	ctx.assertPairProbability(testTime, node1, 100, expectedNodeProb/2)
+}
+
+// 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: {
+			timestamp:  testTime.Add(-time.Hour),
+			pairResult: successPairResult(),
+		},
+		node2: {
+			timestamp:  testTime.Add(-2 * time.Hour),
+			pairResult: failPairResult(0),
+		},
+		node3: {
+			timestamp:  testTime.Add(-3 * time.Hour),
+			pairResult: failPairResult(0),
+		},
+	}
+
+	// We expect the probability for a previously successful channel to
+	// remain high.
+	ctx.assertPairProbability(testTime, node1, 100, 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, expectedNodeProb)
+
+	// For the previously failed connection with node 1, we expect 0.75 *
+	// the node probability = 0.47.
+	ctx.assertPairProbability(testTime, node2, 100, expectedNodeProb*0.75)
+}
diff --git a/routing/result_interpretation_test.go b/routing/result_interpretation_test.go
index 8c4498b06..78b6088ae 100644
--- a/routing/result_interpretation_test.go
+++ b/routing/result_interpretation_test.go
@@ -68,12 +68,8 @@ var resultTestCases = []resultTestCase{
 
 		expectedResult: &interpretedResult{
 			pairResults: map[DirectedNodePair]pairResult{
-				getTestPair(0, 1): {
-					success: true,
-				},
-				getTestPair(1, 2): {
-					minPenalizeAmt: 99,
-				},
+				getTestPair(0, 1): successPairResult(),
+				getTestPair(1, 2): failPairResult(99),
 			},
 		},
 	},
@@ -87,12 +83,12 @@ var resultTestCases = []resultTestCase{
 
 		expectedResult: &interpretedResult{
 			pairResults: map[DirectedNodePair]pairResult{
-				getTestPair(0, 1): {},
-				getTestPair(1, 0): {},
-				getTestPair(1, 2): {},
-				getTestPair(2, 1): {},
-				getTestPair(2, 3): {},
-				getTestPair(3, 2): {},
+				getTestPair(0, 1): failPairResult(0),
+				getTestPair(1, 0): failPairResult(0),
+				getTestPair(1, 2): failPairResult(0),
+				getTestPair(2, 1): failPairResult(0),
+				getTestPair(2, 3): failPairResult(0),
+				getTestPair(3, 2): failPairResult(0),
 			},
 		},
 	},
@@ -107,12 +103,8 @@ var resultTestCases = []resultTestCase{
 
 		expectedResult: &interpretedResult{
 			pairResults: map[DirectedNodePair]pairResult{
-				getTestPair(0, 1): {
-					success: true,
-				},
-				getTestPair(1, 2): {
-					success: true,
-				},
+				getTestPair(0, 1): successPairResult(),
+				getTestPair(1, 2): successPairResult(),
 			},
 			finalFailureReason: &reasonIncorrectDetails,
 		},
@@ -126,9 +118,7 @@ var resultTestCases = []resultTestCase{
 
 		expectedResult: &interpretedResult{
 			pairResults: map[DirectedNodePair]pairResult{
-				getTestPair(0, 1): {
-					success: true,
-				},
+				getTestPair(0, 1): successPairResult(),
 			},
 		},
 	},
@@ -141,12 +131,8 @@ var resultTestCases = []resultTestCase{
 
 		expectedResult: &interpretedResult{
 			pairResults: map[DirectedNodePair]pairResult{
-				getTestPair(0, 1): {
-					success: true,
-				},
-				getTestPair(1, 2): {
-					success: true,
-				},
+				getTestPair(0, 1): successPairResult(),
+				getTestPair(1, 2): successPairResult(),
 			},
 		},
 	},
@@ -160,6 +146,10 @@ var resultTestCases = []resultTestCase{
 
 		expectedResult: &interpretedResult{
 			nodeFailure: &hops[1],
+			pairResults: map[DirectedNodePair]pairResult{
+				getTestPair(1, 0): failPairResult(0),
+				getTestPair(1, 2): failPairResult(0),
+			},
 		},
 	},
 
@@ -174,6 +164,9 @@ var resultTestCases = []resultTestCase{
 		expectedResult: &interpretedResult{
 			finalFailureReason: &reasonError,
 			nodeFailure:        &hops[1],
+			pairResults: map[DirectedNodePair]pairResult{
+				getTestPair(1, 0): failPairResult(0),
+			},
 		},
 	},
 }
diff --git a/routing/router_test.go b/routing/router_test.go
index 0aae52caf..fb73a0005 100644
--- a/routing/router_test.go
+++ b/routing/router_test.go
@@ -91,6 +91,7 @@ func createTestCtxFromGraphInstance(startingHeight uint32, graphInstance *testGr
 	mcConfig := &MissionControlConfig{
 		PenaltyHalfLife:       time.Hour,
 		AprioriHopProbability: 0.9,
+		AprioriWeight:         0.5,
 	}
 
 	mc, err := NewMissionControl(