Merge pull request #2802 from joostjager/probability

routing: probability based path finding
This commit is contained in:
Olaoluwa Osuntokun 2019-06-05 11:30:55 +02:00 committed by GitHub
commit 7453dbeacc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 1600 additions and 423 deletions

@ -0,0 +1,59 @@
// +build routerrpc
package main
import (
"context"
"encoding/hex"
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
"github.com/urfave/cli"
)
var queryMissionControlCommand = cli.Command{
Name: "querymc",
Category: "Payments",
Action: actionDecorator(queryMissionControl),
}
func queryMissionControl(ctx *cli.Context) error {
conn := getClientConn(ctx, false)
defer conn.Close()
client := routerrpc.NewRouterClient(conn)
req := &routerrpc.QueryMissionControlRequest{}
rpcCtx := context.Background()
snapshot, err := client.QueryMissionControl(rpcCtx, req)
if err != nil {
return err
}
type displayNodeHistory struct {
Pubkey string
LastFailTime int64
OtherChanSuccessProb float32
Channels []*routerrpc.ChannelHistory
}
displayResp := struct {
Nodes []displayNodeHistory
}{}
for _, n := range snapshot.Nodes {
displayResp.Nodes = append(
displayResp.Nodes,
displayNodeHistory{
Pubkey: hex.EncodeToString(n.Pubkey),
LastFailTime: n.LastFailTime,
OtherChanSuccessProb: n.OtherChanSuccessProb,
Channels: n.Channels,
},
)
}
printJSON(displayResp)
return nil
}

@ -303,6 +303,7 @@ func main() {
// Add any extra autopilot commands determined by build flags. // Add any extra autopilot commands determined by build flags.
app.Commands = append(app.Commands, autopilotCommands()...) app.Commands = append(app.Commands, autopilotCommands()...)
app.Commands = append(app.Commands, invoicesCommands()...) app.Commands = append(app.Commands, invoicesCommands()...)
app.Commands = append(app.Commands, routerCommands()...)
if err := app.Run(os.Args); err != nil { if err := app.Run(os.Args); err != nil {
fatal(err) fatal(err)

@ -0,0 +1,10 @@
// +build routerrpc
package main
import "github.com/urfave/cli"
// routerCommands will return nil for non-routerrpc builds.
func routerCommands() []cli.Command {
return []cli.Command{queryMissionControlCommand}
}

@ -0,0 +1,10 @@
// +build !routerrpc
package main
import "github.com/urfave/cli"
// routerCommands will return nil for non-routerrpc builds.
func routerCommands() []cli.Command {
return nil
}

@ -27,6 +27,7 @@ import (
"github.com/lightningnetwork/lnd/discovery" "github.com/lightningnetwork/lnd/discovery"
"github.com/lightningnetwork/lnd/htlcswitch/hodl" "github.com/lightningnetwork/lnd/htlcswitch/hodl"
"github.com/lightningnetwork/lnd/lncfg" "github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
"github.com/lightningnetwork/lnd/lnrpc/signrpc" "github.com/lightningnetwork/lnd/lnrpc/signrpc"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing" "github.com/lightningnetwork/lnd/routing"
@ -370,6 +371,7 @@ func loadConfig() (*config, error) {
MaxBackoff: defaultMaxBackoff, MaxBackoff: defaultMaxBackoff,
SubRPCServers: &subRPCServerConfigs{ SubRPCServers: &subRPCServerConfigs{
SignRPC: &signrpc.Config{}, SignRPC: &signrpc.Config{},
RouterRPC: routerrpc.DefaultConfig(),
}, },
Autopilot: &autoPilotConfig{ Autopilot: &autoPilotConfig{
MaxChannels: 5, MaxChannels: 5,

@ -3,7 +3,12 @@
package routerrpc package routerrpc
import ( import (
"time"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/macaroons" "github.com/lightningnetwork/lnd/macaroons"
"github.com/lightningnetwork/lnd/routing" "github.com/lightningnetwork/lnd/routing"
) )
@ -19,6 +24,23 @@ type Config struct {
// directory, named DefaultRouterMacFilename. // directory, named DefaultRouterMacFilename.
RouterMacPath string `long:"routermacaroonpath" description:"Path to the router macaroon"` RouterMacPath string `long:"routermacaroonpath" description:"Path to the router macaroon"`
// MinProbability is the minimum required route success probability to
// attempt the payment.
MinRouteProbability float64 `long:"minrtprob" description:"Minimum required route success probability to attempt the payment"`
// AprioriHopProbability is the assumed success probability of a hop in
// a route when no other information is available.
AprioriHopProbability float64 `long:"apriorihopprob" description:"Assumed success probability of a hop in a route when no other information is available."`
// PenaltyHalfLife defines after how much time a penalized node or
// channel is back at 50% probability.
PenaltyHalfLife time.Duration `long:"penaltyhalflife" description:"Defines the duration after which a penalized node or channel is back at 50% probability"`
// AttemptCost is the virtual cost in path finding weight units of
// executing a payment attempt that fails. It is used to trade off
// potentially better routes against their probability of succeeding.
AttemptCost int64 `long:"attemptcost" description:"The (virtual) cost in sats of a failed payment attempt"`
// NetworkDir is the main network directory wherein the router rpc // NetworkDir is the main network directory wherein the router rpc
// server will find the macaroon named DefaultRouterMacFilename. // server will find the macaroon named DefaultRouterMacFilename.
NetworkDir string NetworkDir string
@ -45,3 +67,28 @@ type Config struct {
// main rpc server. // main rpc server.
RouterBackend *RouterBackend RouterBackend *RouterBackend
} }
// DefaultConfig defines the config defaults.
func DefaultConfig() *Config {
return &Config{
AprioriHopProbability: routing.DefaultAprioriHopProbability,
MinRouteProbability: routing.DefaultMinRouteProbability,
PenaltyHalfLife: routing.DefaultPenaltyHalfLife,
AttemptCost: int64(
routing.DefaultPaymentAttemptPenalty.ToSatoshis(),
),
}
}
// GetMissionControlConfig returns the mission control config based on this sub
// server config.
func GetMissionControlConfig(cfg *Config) *routing.MissionControlConfig {
return &routing.MissionControlConfig{
AprioriHopProbability: cfg.AprioriHopProbability,
MinRouteProbability: cfg.MinRouteProbability,
PaymentAttemptPenalty: lnwire.NewMSatFromSatoshis(
btcutil.Amount(cfg.AttemptCost),
),
PenaltyHalfLife: cfg.PenaltyHalfLife,
}
}

@ -2,6 +2,25 @@
package routerrpc package routerrpc
// Config is the default config for the package. When the build tag isn't import "github.com/lightningnetwork/lnd/routing"
// Config is the default config struct for the package. When the build tag isn't
// specified, then we output a blank config. // specified, then we output a blank config.
type Config struct{} type Config struct{}
// DefaultConfig defines the config defaults. Without the sub server enabled,
// there are no defaults to set.
func DefaultConfig() *Config {
return &Config{}
}
// GetMissionControlConfig returns the mission control config based on this sub
// server config.
func GetMissionControlConfig(cfg *Config) *routing.MissionControlConfig {
return &routing.MissionControlConfig{
AprioriHopProbability: routing.DefaultAprioriHopProbability,
MinRouteProbability: routing.DefaultMinRouteProbability,
PaymentAttemptPenalty: routing.DefaultPaymentAttemptPenalty,
PenaltyHalfLife: routing.DefaultPenaltyHalfLife,
}
}

@ -737,6 +737,275 @@ func (m *ChannelUpdate) GetExtraOpaqueData() []byte {
return nil return nil
} }
type ResetMissionControlRequest struct {
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *ResetMissionControlRequest) Reset() { *m = ResetMissionControlRequest{} }
func (m *ResetMissionControlRequest) String() string { return proto.CompactTextString(m) }
func (*ResetMissionControlRequest) ProtoMessage() {}
func (*ResetMissionControlRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_7a0613f69d37b0a5, []int{8}
}
func (m *ResetMissionControlRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_ResetMissionControlRequest.Unmarshal(m, b)
}
func (m *ResetMissionControlRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_ResetMissionControlRequest.Marshal(b, m, deterministic)
}
func (m *ResetMissionControlRequest) XXX_Merge(src proto.Message) {
xxx_messageInfo_ResetMissionControlRequest.Merge(m, src)
}
func (m *ResetMissionControlRequest) XXX_Size() int {
return xxx_messageInfo_ResetMissionControlRequest.Size(m)
}
func (m *ResetMissionControlRequest) XXX_DiscardUnknown() {
xxx_messageInfo_ResetMissionControlRequest.DiscardUnknown(m)
}
var xxx_messageInfo_ResetMissionControlRequest proto.InternalMessageInfo
type ResetMissionControlResponse struct {
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *ResetMissionControlResponse) Reset() { *m = ResetMissionControlResponse{} }
func (m *ResetMissionControlResponse) String() string { return proto.CompactTextString(m) }
func (*ResetMissionControlResponse) ProtoMessage() {}
func (*ResetMissionControlResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_7a0613f69d37b0a5, []int{9}
}
func (m *ResetMissionControlResponse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_ResetMissionControlResponse.Unmarshal(m, b)
}
func (m *ResetMissionControlResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_ResetMissionControlResponse.Marshal(b, m, deterministic)
}
func (m *ResetMissionControlResponse) XXX_Merge(src proto.Message) {
xxx_messageInfo_ResetMissionControlResponse.Merge(m, src)
}
func (m *ResetMissionControlResponse) XXX_Size() int {
return xxx_messageInfo_ResetMissionControlResponse.Size(m)
}
func (m *ResetMissionControlResponse) XXX_DiscardUnknown() {
xxx_messageInfo_ResetMissionControlResponse.DiscardUnknown(m)
}
var xxx_messageInfo_ResetMissionControlResponse proto.InternalMessageInfo
type QueryMissionControlRequest struct {
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *QueryMissionControlRequest) Reset() { *m = QueryMissionControlRequest{} }
func (m *QueryMissionControlRequest) String() string { return proto.CompactTextString(m) }
func (*QueryMissionControlRequest) ProtoMessage() {}
func (*QueryMissionControlRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_7a0613f69d37b0a5, []int{10}
}
func (m *QueryMissionControlRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_QueryMissionControlRequest.Unmarshal(m, b)
}
func (m *QueryMissionControlRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_QueryMissionControlRequest.Marshal(b, m, deterministic)
}
func (m *QueryMissionControlRequest) XXX_Merge(src proto.Message) {
xxx_messageInfo_QueryMissionControlRequest.Merge(m, src)
}
func (m *QueryMissionControlRequest) XXX_Size() int {
return xxx_messageInfo_QueryMissionControlRequest.Size(m)
}
func (m *QueryMissionControlRequest) XXX_DiscardUnknown() {
xxx_messageInfo_QueryMissionControlRequest.DiscardUnknown(m)
}
var xxx_messageInfo_QueryMissionControlRequest proto.InternalMessageInfo
/// QueryMissionControlResponse contains mission control state per node.
type QueryMissionControlResponse struct {
Nodes []*NodeHistory `protobuf:"bytes,1,rep,name=nodes,proto3" json:"nodes,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *QueryMissionControlResponse) Reset() { *m = QueryMissionControlResponse{} }
func (m *QueryMissionControlResponse) String() string { return proto.CompactTextString(m) }
func (*QueryMissionControlResponse) ProtoMessage() {}
func (*QueryMissionControlResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_7a0613f69d37b0a5, []int{11}
}
func (m *QueryMissionControlResponse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_QueryMissionControlResponse.Unmarshal(m, b)
}
func (m *QueryMissionControlResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_QueryMissionControlResponse.Marshal(b, m, deterministic)
}
func (m *QueryMissionControlResponse) XXX_Merge(src proto.Message) {
xxx_messageInfo_QueryMissionControlResponse.Merge(m, src)
}
func (m *QueryMissionControlResponse) XXX_Size() int {
return xxx_messageInfo_QueryMissionControlResponse.Size(m)
}
func (m *QueryMissionControlResponse) XXX_DiscardUnknown() {
xxx_messageInfo_QueryMissionControlResponse.DiscardUnknown(m)
}
var xxx_messageInfo_QueryMissionControlResponse proto.InternalMessageInfo
func (m *QueryMissionControlResponse) GetNodes() []*NodeHistory {
if m != nil {
return m.Nodes
}
return nil
}
/// NodeHistory contains the mission control state for a particular node.
type NodeHistory struct {
/// Node pubkey
Pubkey []byte `protobuf:"bytes,1,opt,name=pubkey,proto3" json:"pubkey,omitempty"`
/// Time stamp of last failure. Set to zero if no failure happened yet.
LastFailTime int64 `protobuf:"varint,2,opt,name=last_fail_time,proto3" json:"last_fail_time,omitempty"`
/// Estimation of success probability for channels not in the channel array.
OtherChanSuccessProb float32 `protobuf:"fixed32,3,opt,name=other_chan_success_prob,proto3" json:"other_chan_success_prob,omitempty"`
/// Historical information of particular channels.
Channels []*ChannelHistory `protobuf:"bytes,4,rep,name=channels,proto3" json:"channels,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *NodeHistory) Reset() { *m = NodeHistory{} }
func (m *NodeHistory) String() string { return proto.CompactTextString(m) }
func (*NodeHistory) ProtoMessage() {}
func (*NodeHistory) Descriptor() ([]byte, []int) {
return fileDescriptor_7a0613f69d37b0a5, []int{12}
}
func (m *NodeHistory) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_NodeHistory.Unmarshal(m, b)
}
func (m *NodeHistory) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_NodeHistory.Marshal(b, m, deterministic)
}
func (m *NodeHistory) XXX_Merge(src proto.Message) {
xxx_messageInfo_NodeHistory.Merge(m, src)
}
func (m *NodeHistory) XXX_Size() int {
return xxx_messageInfo_NodeHistory.Size(m)
}
func (m *NodeHistory) XXX_DiscardUnknown() {
xxx_messageInfo_NodeHistory.DiscardUnknown(m)
}
var xxx_messageInfo_NodeHistory proto.InternalMessageInfo
func (m *NodeHistory) GetPubkey() []byte {
if m != nil {
return m.Pubkey
}
return nil
}
func (m *NodeHistory) GetLastFailTime() int64 {
if m != nil {
return m.LastFailTime
}
return 0
}
func (m *NodeHistory) GetOtherChanSuccessProb() float32 {
if m != nil {
return m.OtherChanSuccessProb
}
return 0
}
func (m *NodeHistory) GetChannels() []*ChannelHistory {
if m != nil {
return m.Channels
}
return nil
}
/// NodeHistory contains the mission control state for a particular channel.
type ChannelHistory struct {
/// Short channel id
ChannelId uint64 `protobuf:"varint,1,opt,name=channel_id,proto3" json:"channel_id,omitempty"`
/// Time stamp of last failure.
LastFailTime int64 `protobuf:"varint,2,opt,name=last_fail_time,proto3" json:"last_fail_time,omitempty"`
/// Minimum penalization amount.
MinPenalizeAmtSat int64 `protobuf:"varint,3,opt,name=min_penalize_amt_sat,proto3" json:"min_penalize_amt_sat,omitempty"`
/// Estimation of success probability for this channel.
SuccessProb float32 `protobuf:"fixed32,4,opt,name=success_prob,proto3" json:"success_prob,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *ChannelHistory) Reset() { *m = ChannelHistory{} }
func (m *ChannelHistory) String() string { return proto.CompactTextString(m) }
func (*ChannelHistory) ProtoMessage() {}
func (*ChannelHistory) Descriptor() ([]byte, []int) {
return fileDescriptor_7a0613f69d37b0a5, []int{13}
}
func (m *ChannelHistory) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_ChannelHistory.Unmarshal(m, b)
}
func (m *ChannelHistory) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_ChannelHistory.Marshal(b, m, deterministic)
}
func (m *ChannelHistory) XXX_Merge(src proto.Message) {
xxx_messageInfo_ChannelHistory.Merge(m, src)
}
func (m *ChannelHistory) XXX_Size() int {
return xxx_messageInfo_ChannelHistory.Size(m)
}
func (m *ChannelHistory) XXX_DiscardUnknown() {
xxx_messageInfo_ChannelHistory.DiscardUnknown(m)
}
var xxx_messageInfo_ChannelHistory proto.InternalMessageInfo
func (m *ChannelHistory) GetChannelId() uint64 {
if m != nil {
return m.ChannelId
}
return 0
}
func (m *ChannelHistory) GetLastFailTime() int64 {
if m != nil {
return m.LastFailTime
}
return 0
}
func (m *ChannelHistory) GetMinPenalizeAmtSat() int64 {
if m != nil {
return m.MinPenalizeAmtSat
}
return 0
}
func (m *ChannelHistory) GetSuccessProb() float32 {
if m != nil {
return m.SuccessProb
}
return 0
}
func init() { func init() {
proto.RegisterEnum("routerrpc.Failure_FailureCode", Failure_FailureCode_name, Failure_FailureCode_value) proto.RegisterEnum("routerrpc.Failure_FailureCode", Failure_FailureCode_name, Failure_FailureCode_value)
proto.RegisterType((*PaymentRequest)(nil), "routerrpc.PaymentRequest") proto.RegisterType((*PaymentRequest)(nil), "routerrpc.PaymentRequest")
@ -747,89 +1016,109 @@ func init() {
proto.RegisterType((*SendToRouteResponse)(nil), "routerrpc.SendToRouteResponse") proto.RegisterType((*SendToRouteResponse)(nil), "routerrpc.SendToRouteResponse")
proto.RegisterType((*Failure)(nil), "routerrpc.Failure") proto.RegisterType((*Failure)(nil), "routerrpc.Failure")
proto.RegisterType((*ChannelUpdate)(nil), "routerrpc.ChannelUpdate") proto.RegisterType((*ChannelUpdate)(nil), "routerrpc.ChannelUpdate")
proto.RegisterType((*ResetMissionControlRequest)(nil), "routerrpc.ResetMissionControlRequest")
proto.RegisterType((*ResetMissionControlResponse)(nil), "routerrpc.ResetMissionControlResponse")
proto.RegisterType((*QueryMissionControlRequest)(nil), "routerrpc.QueryMissionControlRequest")
proto.RegisterType((*QueryMissionControlResponse)(nil), "routerrpc.QueryMissionControlResponse")
proto.RegisterType((*NodeHistory)(nil), "routerrpc.NodeHistory")
proto.RegisterType((*ChannelHistory)(nil), "routerrpc.ChannelHistory")
} }
func init() { proto.RegisterFile("routerrpc/router.proto", fileDescriptor_7a0613f69d37b0a5) } func init() { proto.RegisterFile("routerrpc/router.proto", fileDescriptor_7a0613f69d37b0a5) }
var fileDescriptor_7a0613f69d37b0a5 = []byte{ var fileDescriptor_7a0613f69d37b0a5 = []byte{
// 1224 bytes of a gzipped FileDescriptorProto // 1456 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0x56, 0xef, 0x72, 0x1a, 0x37, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x56, 0xdb, 0x72, 0x1a, 0x47,
0x10, 0x2f, 0xb1, 0xcd, 0x9f, 0x05, 0xcc, 0x59, 0xb6, 0x13, 0x4c, 0xe2, 0xc4, 0xa1, 0x9d, 0xd6, 0x13, 0xfe, 0x31, 0x12, 0x87, 0xe6, 0xa0, 0xd5, 0xe8, 0x60, 0x84, 0x2c, 0x5b, 0xde, 0xff, 0xff,
0x93, 0xe9, 0xd8, 0x53, 0x3a, 0xc9, 0xc7, 0x76, 0x08, 0x88, 0xfa, 0x26, 0x70, 0x47, 0x75, 0xe0, 0x1d, 0x95, 0xcb, 0x25, 0x55, 0x48, 0xd9, 0x95, 0xab, 0xa4, 0x30, 0x2c, 0xd1, 0x96, 0x60, 0xc1,
0xc4, 0xed, 0x07, 0x8d, 0xcc, 0xc9, 0x70, 0x35, 0xf7, 0xc7, 0x77, 0xa2, 0xb5, 0x5f, 0xa0, 0xaf, 0x03, 0xc8, 0x56, 0x72, 0x31, 0x35, 0x62, 0x47, 0xb0, 0x11, 0xec, 0xae, 0x76, 0x87, 0xc4, 0xe4,
0xd3, 0xa7, 0xe8, 0x43, 0xf4, 0x11, 0xfa, 0x16, 0x1d, 0x49, 0x77, 0x80, 0x1d, 0xf7, 0x13, 0xa7, 0x01, 0xf2, 0x3a, 0xc9, 0x4d, 0x6e, 0x73, 0x97, 0x87, 0xc8, 0xdb, 0xa4, 0x66, 0x66, 0x97, 0x83,
0xdf, 0xef, 0xa7, 0x5d, 0xed, 0x6a, 0x77, 0x05, 0x3c, 0x8d, 0xc3, 0x85, 0xe0, 0x71, 0x1c, 0x4d, 0x8c, 0x92, 0x5c, 0xc1, 0x7e, 0xdf, 0x37, 0xdd, 0xd3, 0x3d, 0xdd, 0x3d, 0x03, 0xfb, 0x81, 0x37,
0x4e, 0xf5, 0xd7, 0x49, 0x14, 0x87, 0x22, 0x44, 0xa5, 0x25, 0xde, 0x28, 0xc5, 0xd1, 0x44, 0xa3, 0xe5, 0x2c, 0x08, 0xfc, 0xc1, 0x99, 0xfa, 0x77, 0xea, 0x07, 0x1e, 0xf7, 0x50, 0x76, 0x8e, 0x97,
0xcd, 0xbf, 0x73, 0xb0, 0x3d, 0x64, 0x77, 0x3e, 0x0f, 0x04, 0xe1, 0x37, 0x0b, 0x9e, 0x08, 0xf4, 0xb3, 0x81, 0x3f, 0x50, 0xa8, 0xfe, 0x47, 0x02, 0x8a, 0x1d, 0x3a, 0x9b, 0x30, 0x97, 0x63, 0x76,
0x0c, 0x0a, 0x11, 0xbb, 0xa3, 0x31, 0xbf, 0xa9, 0xe7, 0x8e, 0x72, 0xc7, 0x25, 0x92, 0x8f, 0xd8, 0x37, 0x65, 0x21, 0x47, 0x8f, 0x21, 0xed, 0xd3, 0x19, 0x09, 0xd8, 0x5d, 0x29, 0x71, 0x9c, 0x38,
0x1d, 0xe1, 0x37, 0xa8, 0x09, 0xd5, 0x2b, 0xce, 0xe9, 0xdc, 0xf3, 0x3d, 0x41, 0x13, 0x26, 0xea, 0xc9, 0xe2, 0x94, 0x4f, 0x67, 0x98, 0xdd, 0x21, 0x1d, 0x0a, 0x37, 0x8c, 0x91, 0xb1, 0x33, 0x71,
0x4f, 0x8e, 0x72, 0xc7, 0x1b, 0xa4, 0x7c, 0xc5, 0x79, 0x5f, 0x62, 0x0e, 0x13, 0xe8, 0x10, 0x60, 0x38, 0x09, 0x29, 0x2f, 0x3d, 0x3a, 0x4e, 0x9c, 0x24, 0x71, 0xee, 0x86, 0xb1, 0xa6, 0xc0, 0xba,
0x32, 0x17, 0xbf, 0x6b, 0x51, 0x7d, 0xe3, 0x28, 0x77, 0xbc, 0x45, 0x4a, 0x12, 0x51, 0x0a, 0xf4, 0x94, 0xa3, 0x23, 0x80, 0xc1, 0x98, 0xff, 0xa0, 0x44, 0xa5, 0xe4, 0x71, 0xe2, 0x64, 0x13, 0x67,
0x0d, 0xd4, 0x84, 0xe7, 0xf3, 0x70, 0x21, 0x68, 0xc2, 0x27, 0x61, 0xe0, 0x26, 0xf5, 0x4d, 0xa5, 0x05, 0x22, 0x15, 0xe8, 0x33, 0xd8, 0xe2, 0xce, 0x84, 0x79, 0x53, 0x4e, 0x42, 0x36, 0xf0, 0x5c,
0xd9, 0x4e, 0x61, 0x47, 0xa3, 0xe8, 0x04, 0x76, 0xc3, 0x85, 0x98, 0x86, 0x5e, 0x30, 0xa5, 0x93, 0x3b, 0x2c, 0x6d, 0x48, 0x4d, 0x31, 0x82, 0xbb, 0x0a, 0x45, 0xa7, 0xb0, 0xe3, 0x4d, 0xf9, 0xd0,
0x19, 0x0b, 0x02, 0x3e, 0xa7, 0x9e, 0x5b, 0xdf, 0x52, 0x1e, 0x77, 0x32, 0xaa, 0xa3, 0x19, 0xd3, 0x73, 0xdc, 0x21, 0x19, 0x8c, 0xa8, 0xeb, 0xb2, 0x31, 0x71, 0xec, 0xd2, 0xa6, 0xf4, 0xb8, 0x1d,
0x6d, 0xfe, 0x06, 0xb5, 0x65, 0x18, 0x49, 0x14, 0x06, 0x09, 0x47, 0x07, 0x50, 0x94, 0x71, 0xcc, 0x53, 0x35, 0xc5, 0x98, 0xb6, 0xfe, 0x3d, 0x6c, 0xcd, 0xc3, 0x08, 0x7d, 0xcf, 0x0d, 0x19, 0x3a,
0x58, 0x32, 0x53, 0x81, 0x54, 0x88, 0x8c, 0xeb, 0x8c, 0x25, 0x33, 0xf4, 0x1c, 0x4a, 0x51, 0xcc, 0x80, 0x8c, 0x88, 0x63, 0x44, 0xc3, 0x91, 0x0c, 0x24, 0x8f, 0x45, 0x5c, 0xe7, 0x34, 0x1c, 0xa1,
0xa9, 0xe7, 0xb3, 0x29, 0x57, 0x51, 0x54, 0x48, 0x31, 0x8a, 0xb9, 0x29, 0xd7, 0xe8, 0x15, 0x94, 0x43, 0xc8, 0xfa, 0x01, 0x23, 0xce, 0x84, 0x0e, 0x99, 0x8c, 0x22, 0x8f, 0x33, 0x7e, 0xc0, 0x4c,
0x23, 0x6d, 0x8a, 0xf2, 0x38, 0x56, 0x31, 0x94, 0x08, 0xa4, 0x10, 0x8e, 0xe3, 0xe6, 0x0f, 0x50, 0xf1, 0x8d, 0x9e, 0x41, 0xce, 0x57, 0xa6, 0x08, 0x0b, 0x02, 0x19, 0x43, 0x16, 0x43, 0x04, 0x19,
0x23, 0x32, 0x97, 0x3d, 0xce, 0xb3, 0x9c, 0x21, 0xd8, 0x74, 0x79, 0x22, 0x52, 0x3f, 0xea, 0x5b, 0x41, 0xa0, 0x7f, 0x05, 0x5b, 0x58, 0xe4, 0xb2, 0xc1, 0x58, 0x9c, 0x33, 0x04, 0x1b, 0x36, 0x0b,
0xe6, 0x91, 0xf9, 0xeb, 0x89, 0xca, 0x33, 0x5f, 0xe6, 0xa8, 0xe9, 0x82, 0xb1, 0xda, 0x9f, 0x1e, 0x79, 0xe4, 0x47, 0xfe, 0x17, 0x79, 0xa4, 0x93, 0xe5, 0x44, 0xa5, 0xe8, 0x44, 0xe4, 0x48, 0xb7,
0xf6, 0x18, 0x0c, 0x79, 0x3f, 0x32, 0x5c, 0x99, 0x63, 0x5f, 0xee, 0xca, 0xa9, 0x5d, 0xdb, 0x29, 0x41, 0x5b, 0xac, 0x8f, 0x36, 0x7b, 0x02, 0x9a, 0x38, 0x1f, 0x11, 0xae, 0xc8, 0xf1, 0x44, 0xac,
0xde, 0xe3, 0x7c, 0x90, 0x30, 0x81, 0xbe, 0xd6, 0x29, 0xa4, 0xf3, 0x70, 0x72, 0x4d, 0x5d, 0x3e, 0x4a, 0xc8, 0x55, 0xc5, 0x08, 0x6f, 0x30, 0xd6, 0x0a, 0x29, 0x47, 0x2f, 0x54, 0x0a, 0xc9, 0xd8,
0x67, 0x77, 0xa9, 0xf9, 0xaa, 0x84, 0xfb, 0xe1, 0xe4, 0xba, 0x2b, 0xc1, 0xe6, 0xaf, 0x80, 0x1c, 0x1b, 0xdc, 0x12, 0x9b, 0x8d, 0xe9, 0x2c, 0x32, 0x5f, 0x10, 0x70, 0xd3, 0x1b, 0xdc, 0xd6, 0x05,
0x1e, 0xb8, 0xa3, 0x50, 0xf9, 0xca, 0x0e, 0xfa, 0x1a, 0x2a, 0x59, 0x70, 0x6b, 0x89, 0xc9, 0x02, 0xa8, 0x7f, 0x07, 0xa8, 0xcb, 0x5c, 0xbb, 0xe7, 0x49, 0x5f, 0xf1, 0x46, 0x9f, 0x43, 0x3e, 0x0e,
0x56, 0xc9, 0x69, 0xc2, 0x96, 0x2a, 0x15, 0x65, 0xb6, 0xdc, 0xaa, 0x9c, 0xcc, 0x03, 0x59, 0x2f, 0x6e, 0x29, 0x31, 0x71, 0xc0, 0x32, 0x39, 0x3a, 0x6c, 0xca, 0x52, 0x91, 0x66, 0x73, 0x95, 0xfc,
0xda, 0x8c, 0xa6, 0x9a, 0x14, 0x76, 0xef, 0x19, 0x4f, 0xa3, 0x68, 0x80, 0x4c, 0xa3, 0x4e, 0x6b, 0xe9, 0xd8, 0x15, 0xf5, 0xa2, 0xcc, 0x28, 0x4a, 0x27, 0xb0, 0xb3, 0x62, 0x3c, 0x8a, 0xa2, 0x0c,
0x6e, 0x99, 0x56, 0xb5, 0x46, 0xdf, 0x42, 0xe1, 0x8a, 0x79, 0xf3, 0x45, 0x9c, 0x19, 0x46, 0x27, 0x22, 0x8d, 0x2a, 0xad, 0x89, 0x79, 0x5a, 0xe5, 0x37, 0x7a, 0x05, 0xe9, 0x1b, 0xea, 0x8c, 0xa7,
0xcb, 0x8a, 0x3c, 0xe9, 0x69, 0x86, 0x64, 0x92, 0xe6, 0x9f, 0x05, 0x28, 0xa4, 0x20, 0x6a, 0xc1, 0x41, 0x6c, 0x18, 0x9d, 0xce, 0x2b, 0xf2, 0xb4, 0xa1, 0x18, 0x1c, 0x4b, 0xf4, 0x9f, 0xd3, 0x90,
0xe6, 0x24, 0x74, 0xb5, 0xc5, 0xed, 0xd6, 0xcb, 0xcf, 0xb7, 0x65, 0xbf, 0x9d, 0xd0, 0xe5, 0x44, 0x8e, 0x40, 0x54, 0x81, 0x8d, 0x81, 0x67, 0x2b, 0x8b, 0xc5, 0xca, 0xd3, 0x4f, 0x97, 0xc5, 0xbf,
0x69, 0x51, 0x0b, 0xf6, 0x53, 0x53, 0x34, 0x09, 0x17, 0xf1, 0x84, 0xd3, 0x68, 0x71, 0x79, 0xcd, 0x35, 0xcf, 0x66, 0x58, 0x6a, 0x51, 0x05, 0xf6, 0x22, 0x53, 0x24, 0xf4, 0xa6, 0xc1, 0x80, 0x11,
0xef, 0xd2, 0xdb, 0xde, 0x4d, 0x49, 0x47, 0x71, 0x43, 0x45, 0xa1, 0x1f, 0x61, 0x3b, 0x2b, 0xb5, 0x7f, 0x7a, 0x7d, 0xcb, 0x66, 0xd1, 0x69, 0xef, 0x44, 0x64, 0x57, 0x72, 0x1d, 0x49, 0xa1, 0xaf,
0x45, 0xe4, 0x32, 0xc1, 0xd5, 0xdd, 0x97, 0x5b, 0xf5, 0x35, 0x8f, 0x69, 0xc5, 0x8d, 0x15, 0x4f, 0xa1, 0x18, 0x97, 0xda, 0xd4, 0xb7, 0x29, 0x67, 0xf2, 0xec, 0x73, 0x95, 0xd2, 0x92, 0xc7, 0xa8,
0xaa, 0x93, 0xf5, 0xa5, 0x2c, 0xab, 0x99, 0x98, 0x4f, 0xf4, 0xed, 0xc9, 0xba, 0xde, 0x24, 0x45, 0xe2, 0xfa, 0x92, 0xc7, 0x85, 0xc1, 0xf2, 0xa7, 0x28, 0xab, 0x11, 0x1f, 0x0f, 0xd4, 0xe9, 0x89,
0x09, 0xa8, 0x7b, 0x6b, 0x42, 0x35, 0x0c, 0xbc, 0x30, 0xa0, 0xc9, 0x8c, 0xd1, 0xd6, 0xdb, 0x77, 0xba, 0xde, 0xc0, 0x19, 0x01, 0xc8, 0x73, 0xd3, 0xa1, 0xe0, 0xb9, 0x8e, 0xe7, 0x92, 0x70, 0x44,
0xaa, 0x96, 0x2b, 0xa4, 0xac, 0x40, 0x67, 0xc6, 0x5a, 0x6f, 0xdf, 0xc9, 0xd2, 0x53, 0xdd, 0xc3, 0x49, 0xe5, 0xf5, 0x1b, 0x59, 0xcb, 0x79, 0x9c, 0x93, 0x60, 0x77, 0x44, 0x2b, 0xaf, 0xdf, 0x88,
0x6f, 0x23, 0x2f, 0xbe, 0xab, 0xe7, 0x8f, 0x72, 0xc7, 0x55, 0xa2, 0x1a, 0x0a, 0x2b, 0x04, 0xed, 0xd2, 0x93, 0xdd, 0xc3, 0x3e, 0xfa, 0x4e, 0x30, 0x2b, 0xa5, 0x8e, 0x13, 0x27, 0x05, 0x2c, 0x1b,
0xc1, 0xd6, 0xd5, 0x9c, 0x4d, 0x93, 0x7a, 0x41, 0x51, 0x7a, 0xd1, 0xfc, 0x67, 0x13, 0xca, 0x6b, 0xca, 0x90, 0x08, 0xda, 0x85, 0xcd, 0x9b, 0x31, 0x1d, 0x86, 0xa5, 0xb4, 0xa4, 0xd4, 0x87, 0xfe,
0x29, 0x40, 0x15, 0x28, 0x12, 0xec, 0x60, 0x72, 0x8e, 0xbb, 0xc6, 0x17, 0xa8, 0x0e, 0x7b, 0x63, 0xe7, 0x06, 0xe4, 0x96, 0x52, 0x80, 0xf2, 0x90, 0xc1, 0x46, 0xd7, 0xc0, 0x97, 0x46, 0x5d, 0xfb,
0xeb, 0x83, 0x65, 0x7f, 0xb4, 0xe8, 0xb0, 0x7d, 0x31, 0xc0, 0xd6, 0x88, 0x9e, 0xb5, 0x9d, 0x33, 0x0f, 0x2a, 0xc1, 0x6e, 0xdf, 0xba, 0xb0, 0xda, 0xef, 0x2d, 0xd2, 0xa9, 0x5e, 0xb5, 0x0c, 0xab,
0x23, 0x87, 0x5e, 0x40, 0xdd, 0xb4, 0x3a, 0x36, 0x21, 0xb8, 0x33, 0x5a, 0x72, 0xed, 0x81, 0x3d, 0x47, 0xce, 0xab, 0xdd, 0x73, 0x2d, 0x81, 0x9e, 0x40, 0xc9, 0xb4, 0x6a, 0x6d, 0x8c, 0x8d, 0x5a,
0xb6, 0x46, 0xc6, 0x13, 0xf4, 0x0a, 0x9e, 0xf7, 0x4c, 0xab, 0xdd, 0xa7, 0x2b, 0x4d, 0xa7, 0x3f, 0x6f, 0xce, 0x55, 0x5b, 0xed, 0xbe, 0xd5, 0xd3, 0x1e, 0xa1, 0x67, 0x70, 0xd8, 0x30, 0xad, 0x6a,
0x3a, 0xa7, 0xf8, 0xd3, 0xd0, 0x24, 0x17, 0xc6, 0xc6, 0x63, 0x82, 0xb3, 0x51, 0xbf, 0x93, 0x59, 0x93, 0x2c, 0x34, 0xb5, 0x66, 0xef, 0x92, 0x18, 0x1f, 0x3a, 0x26, 0xbe, 0xd2, 0x92, 0xeb, 0x04,
0xd8, 0x44, 0x07, 0xb0, 0xaf, 0x05, 0x7a, 0x0b, 0x1d, 0xd9, 0x36, 0x75, 0x6c, 0xdb, 0x32, 0xb6, 0xe7, 0xbd, 0x66, 0x2d, 0xb6, 0xb0, 0x81, 0x0e, 0x60, 0x4f, 0x09, 0xd4, 0x12, 0xd2, 0x6b, 0xb7,
0xd0, 0x0e, 0x54, 0x4d, 0xeb, 0xbc, 0xdd, 0x37, 0xbb, 0x94, 0xe0, 0x76, 0x7f, 0x60, 0xe4, 0xd1, 0x49, 0xb7, 0xdd, 0xb6, 0xb4, 0x4d, 0xb4, 0x0d, 0x05, 0xd3, 0xba, 0xac, 0x36, 0xcd, 0x3a, 0xc1,
0x2e, 0xd4, 0x1e, 0xea, 0x0a, 0xd2, 0x44, 0xa6, 0xb3, 0x2d, 0xd3, 0xb6, 0xe8, 0x39, 0x26, 0x8e, 0x46, 0xb5, 0xd9, 0xd2, 0x52, 0x68, 0x07, 0xb6, 0xee, 0xeb, 0xd2, 0xc2, 0x44, 0xac, 0x6b, 0x5b,
0x69, 0x5b, 0x46, 0x11, 0x3d, 0x05, 0x74, 0x9f, 0x3a, 0x1b, 0xb4, 0x3b, 0x46, 0x09, 0xed, 0xc3, 0x66, 0xdb, 0x22, 0x97, 0x06, 0xee, 0x9a, 0x6d, 0x4b, 0xcb, 0xa0, 0x7d, 0x40, 0xab, 0xd4, 0x79,
0xce, 0x7d, 0xfc, 0x03, 0xbe, 0x30, 0x40, 0xa6, 0x41, 0x1f, 0x8c, 0xbe, 0xc7, 0x7d, 0xfb, 0x23, 0xab, 0x5a, 0xd3, 0xb2, 0x68, 0x0f, 0xb6, 0x57, 0xf1, 0x0b, 0xe3, 0x4a, 0x03, 0x91, 0x06, 0xb5,
0x1d, 0x98, 0x96, 0x39, 0x18, 0x0f, 0x8c, 0x32, 0xda, 0x03, 0xa3, 0x87, 0x31, 0x35, 0x2d, 0x67, 0x31, 0xf2, 0xd6, 0x68, 0xb6, 0xdf, 0x93, 0x96, 0x69, 0x99, 0xad, 0x7e, 0x4b, 0xcb, 0xa1, 0x5d,
0xdc, 0xeb, 0x99, 0x1d, 0x13, 0x5b, 0x23, 0xa3, 0xa2, 0x3d, 0x3f, 0x16, 0x78, 0x55, 0x6e, 0xe8, 0xd0, 0x1a, 0x86, 0x41, 0x4c, 0xab, 0xdb, 0x6f, 0x34, 0xcc, 0x9a, 0x69, 0x58, 0x3d, 0x2d, 0xaf,
0x9c, 0xb5, 0x2d, 0x0b, 0xf7, 0x69, 0xd7, 0x74, 0xda, 0xef, 0xfb, 0xb8, 0x6b, 0x6c, 0xa3, 0x43, 0x3c, 0xaf, 0x0b, 0xbc, 0x20, 0x16, 0xd4, 0xce, 0xab, 0x96, 0x65, 0x34, 0x49, 0xdd, 0xec, 0x56,
0x38, 0x18, 0xe1, 0xc1, 0xd0, 0x26, 0x6d, 0x72, 0x41, 0x33, 0xbe, 0xd7, 0x36, 0xfb, 0x63, 0x82, 0xdf, 0x36, 0x8d, 0xba, 0x56, 0x44, 0x47, 0x70, 0xd0, 0x33, 0x5a, 0x9d, 0x36, 0xae, 0xe2, 0x2b,
0x8d, 0x1a, 0x7a, 0x0d, 0x87, 0x04, 0xff, 0x3c, 0x36, 0x09, 0xee, 0x52, 0xcb, 0xee, 0x62, 0xda, 0x12, 0xf3, 0x8d, 0xaa, 0xd9, 0xec, 0x63, 0x43, 0xdb, 0x42, 0xcf, 0xe1, 0x08, 0x1b, 0xef, 0xfa,
0xc3, 0xed, 0xd1, 0x98, 0x60, 0x3a, 0x30, 0x1d, 0xc7, 0xb4, 0x7e, 0x32, 0x0c, 0xf4, 0x15, 0x1c, 0x26, 0x36, 0xea, 0xc4, 0x6a, 0xd7, 0x0d, 0xd2, 0x30, 0xaa, 0xbd, 0x3e, 0x36, 0x48, 0xcb, 0xec,
0x2d, 0x25, 0x4b, 0x03, 0x0f, 0x54, 0x3b, 0x32, 0xbe, 0xec, 0x3e, 0x2d, 0xfc, 0x69, 0x44, 0x87, 0x76, 0x4d, 0xeb, 0x1b, 0x4d, 0x43, 0xff, 0x83, 0xe3, 0xb9, 0x64, 0x6e, 0xe0, 0x9e, 0x6a, 0x5b,
0x18, 0x13, 0x03, 0xa1, 0x06, 0x3c, 0x5d, 0xb9, 0xd7, 0x0e, 0x52, 0xdf, 0xbb, 0x92, 0x1b, 0x62, 0xc4, 0x17, 0x9f, 0xa7, 0x65, 0x7c, 0xe8, 0x91, 0x8e, 0x61, 0x60, 0x0d, 0xa1, 0x32, 0xec, 0x2f,
0x32, 0x68, 0x5b, 0xf2, 0x82, 0xef, 0x71, 0x7b, 0xf2, 0xd8, 0x2b, 0xee, 0xe1, 0xb1, 0xf7, 0x9b, 0xdc, 0x2b, 0x07, 0x91, 0xef, 0x1d, 0xc1, 0x75, 0x0c, 0xdc, 0xaa, 0x5a, 0xe2, 0x80, 0x57, 0xb8,
0x7f, 0x6d, 0x40, 0xf5, 0x5e, 0xd1, 0xa3, 0x17, 0x50, 0x4a, 0xbc, 0x69, 0xc0, 0x84, 0x6c, 0x65, 0x5d, 0xb1, 0xed, 0x05, 0x77, 0x7f, 0xdb, 0x7b, 0xfa, 0x2f, 0x49, 0x28, 0xac, 0x14, 0x3d, 0x7a,
0xdd, 0xe5, 0x2b, 0x40, 0x3d, 0x00, 0x33, 0xe6, 0x05, 0x7a, 0xbc, 0xe8, 0x6e, 0x2b, 0x29, 0x44, 0x02, 0xd9, 0xd0, 0x19, 0xba, 0x94, 0x8b, 0x56, 0x56, 0x5d, 0xbe, 0x00, 0xe4, 0x05, 0x30, 0xa2,
0x0d, 0x97, 0x67, 0x50, 0x90, 0x3d, 0x23, 0x67, 0xf9, 0x86, 0x6a, 0x90, 0xbc, 0x5c, 0x9a, 0xae, 0x8e, 0xab, 0xc6, 0x8b, 0xea, 0xb6, 0xac, 0x44, 0xe4, 0x70, 0x79, 0x0c, 0x69, 0xd1, 0x33, 0x62,
0xb4, 0x2a, 0xe7, 0x57, 0x22, 0x98, 0x1f, 0xa9, 0xde, 0xa9, 0x92, 0x15, 0x80, 0xbe, 0x84, 0xaa, 0x96, 0x27, 0x65, 0x83, 0xa4, 0xc4, 0xa7, 0x69, 0x0b, 0xab, 0x62, 0x7e, 0x85, 0x9c, 0x4e, 0x7c,
0xcf, 0x93, 0x84, 0x4d, 0x39, 0xd5, 0xf5, 0x0f, 0x4a, 0x51, 0x49, 0xc1, 0x9e, 0xc4, 0xa4, 0x28, 0xd9, 0x3b, 0x05, 0xbc, 0x00, 0xd0, 0x7f, 0xa1, 0x30, 0x61, 0x61, 0x48, 0x87, 0x8c, 0xa8, 0xfa,
0xeb, 0x5f, 0x2d, 0xda, 0xd2, 0xa2, 0x14, 0xd4, 0xa2, 0x87, 0xe3, 0x53, 0xb0, 0xb4, 0xcd, 0xd6, 0x07, 0xa9, 0xc8, 0x47, 0x60, 0x43, 0x60, 0x42, 0x14, 0xf7, 0xaf, 0x12, 0x6d, 0x2a, 0x51, 0x04,
0xc7, 0xa7, 0x60, 0xe8, 0x0d, 0xec, 0xe8, 0x5e, 0xf6, 0x02, 0xcf, 0x5f, 0xf8, 0xba, 0xa7, 0x0b, 0x2a, 0xd1, 0xfd, 0xf1, 0xc9, 0x69, 0xd4, 0x66, 0xcb, 0xe3, 0x93, 0x53, 0xf4, 0x12, 0xb6, 0x55,
0xea, 0xc8, 0x35, 0xd5, 0xd3, 0x1a, 0x57, 0xad, 0x7d, 0x00, 0xc5, 0x4b, 0x96, 0x70, 0x39, 0xb9, 0x2f, 0x3b, 0xae, 0x33, 0x99, 0x4e, 0x54, 0x4f, 0xa7, 0xe5, 0x96, 0xb7, 0x64, 0x4f, 0x2b, 0x5c,
0xeb, 0x45, 0x65, 0xac, 0x20, 0xd7, 0x3d, 0xae, 0x1e, 0x21, 0x39, 0xcf, 0x63, 0x39, 0x4d, 0x4a, 0xb6, 0xf6, 0x01, 0x64, 0xae, 0x69, 0xc8, 0xc4, 0xe4, 0x2e, 0x65, 0xa4, 0xb1, 0xb4, 0xf8, 0x6e,
0x9a, 0xba, 0xe2, 0x9c, 0xc8, 0x3c, 0x2e, 0x3d, 0xb0, 0xdb, 0x95, 0x87, 0xf2, 0x9a, 0x07, 0x8d, 0x30, 0x79, 0x09, 0x89, 0x79, 0x1e, 0x88, 0x69, 0x92, 0x55, 0xd4, 0x0d, 0x63, 0x58, 0xe4, 0x71,
0x2b, 0x0f, 0x6f, 0x60, 0x87, 0xdf, 0x8a, 0x98, 0xd1, 0x30, 0x62, 0x37, 0x0b, 0x4e, 0x5d, 0x26, 0xee, 0x81, 0x7e, 0x5c, 0x78, 0xc8, 0x2d, 0x79, 0x50, 0xb8, 0xf4, 0xf0, 0x12, 0xb6, 0xd9, 0x47,
0x58, 0xbd, 0xa2, 0x92, 0x5b, 0x53, 0x84, 0xad, 0xf0, 0x2e, 0x13, 0xac, 0xf5, 0x6f, 0x0e, 0xf2, 0x1e, 0x50, 0xe2, 0xf9, 0xf4, 0x6e, 0xca, 0x88, 0x4d, 0x39, 0x2d, 0xe5, 0x65, 0x72, 0xb7, 0x24,
0x6a, 0x2c, 0xc7, 0xa8, 0x0b, 0x65, 0x39, 0xa6, 0xd3, 0x97, 0x11, 0x1d, 0xac, 0x0d, 0xb2, 0xfb, 0xd1, 0x96, 0x78, 0x9d, 0x72, 0xaa, 0x3f, 0x81, 0x32, 0x66, 0x21, 0xe3, 0x2d, 0x27, 0x0c, 0x1d,
0x8f, 0x7e, 0xa3, 0xf1, 0x18, 0x95, 0x4e, 0xf5, 0x0f, 0x60, 0xe0, 0x44, 0x78, 0xbe, 0x9c, 0x78, 0xcf, 0xad, 0x79, 0x2e, 0x0f, 0xbc, 0x71, 0x74, 0x01, 0xe8, 0x47, 0x70, 0xb8, 0x96, 0x55, 0x13,
0xe9, 0xbb, 0x85, 0xd6, 0xf5, 0x0f, 0x1e, 0xc3, 0xc6, 0xf3, 0x47, 0xb9, 0xd4, 0x58, 0x5f, 0x1f, 0x5c, 0x2c, 0x7e, 0x37, 0x65, 0xc1, 0x6c, 0xfd, 0xe2, 0x0b, 0x38, 0x5c, 0xcb, 0x46, 0xe3, 0xff,
0x29, 0x7d, 0x39, 0xd0, 0xe1, 0x9a, 0xf6, 0xf3, 0xe7, 0xaa, 0xf1, 0xf2, 0xff, 0x68, 0x6d, 0xed, 0x15, 0x6c, 0xba, 0x9e, 0xcd, 0xc2, 0x52, 0xe2, 0x38, 0x79, 0x92, 0xab, 0xec, 0x2f, 0xcd, 0x4d,
0xfd, 0x77, 0xbf, 0x9c, 0x4e, 0x3d, 0x31, 0x5b, 0x5c, 0x9e, 0x4c, 0x42, 0xff, 0x74, 0xee, 0x4d, 0xcb, 0xb3, 0xd9, 0xb9, 0x13, 0x72, 0x2f, 0x98, 0x61, 0x25, 0xd2, 0x7f, 0x4f, 0x40, 0x6e, 0x09,
0x67, 0x22, 0xf0, 0x82, 0x69, 0xc0, 0xc5, 0x1f, 0x61, 0x7c, 0x7d, 0x3a, 0x0f, 0xdc, 0x53, 0xf5, 0x46, 0xfb, 0x90, 0x8a, 0x66, 0xb4, 0x2a, 0xaa, 0xe8, 0x0b, 0xbd, 0x80, 0xe2, 0x98, 0x86, 0x9c,
0x7a, 0x9d, 0x2e, 0xcd, 0x5c, 0xe6, 0xd5, 0x1f, 0x9f, 0xef, 0xff, 0x0b, 0x00, 0x00, 0xff, 0xff, 0x88, 0x91, 0x4d, 0xc4, 0x21, 0x45, 0xf7, 0xdd, 0x3d, 0x14, 0x7d, 0x09, 0x8f, 0x3d, 0x3e, 0x62,
0x89, 0x21, 0x7b, 0xbd, 0x28, 0x09, 0x00, 0x00, 0x81, 0x7c, 0x2f, 0x90, 0x70, 0x3a, 0x18, 0xb0, 0x30, 0x24, 0x7e, 0xe0, 0x5d, 0xcb, 0x52, 0x7b,
0x84, 0x1f, 0xa2, 0xd1, 0x6b, 0xc8, 0x44, 0x35, 0x22, 0x9e, 0x23, 0x62, 0xeb, 0x07, 0x9f, 0x8e,
0xfc, 0x78, 0xf7, 0x73, 0xa9, 0xfe, 0x6b, 0x02, 0x8a, 0xab, 0x24, 0x7a, 0x2a, 0xab, 0x3f, 0x7e,
0xad, 0x24, 0xe4, 0x61, 0x2e, 0x21, 0xff, 0x3a, 0x96, 0x0a, 0xec, 0x4e, 0x1c, 0x97, 0xf8, 0xcc,
0xa5, 0x63, 0xe7, 0x27, 0x46, 0xe2, 0x87, 0x44, 0x52, 0xaa, 0xd7, 0x72, 0x48, 0x87, 0xfc, 0x4a,
0xd0, 0x1b, 0x32, 0xe8, 0x15, 0xac, 0xf2, 0x5b, 0x12, 0x52, 0xf2, 0xca, 0x0e, 0x50, 0x1d, 0x72,
0xe2, 0x0a, 0x8f, 0x5e, 0x4d, 0x68, 0x39, 0xe2, 0xd5, 0x07, 0x61, 0xb9, 0xbc, 0x8e, 0x8a, 0x8e,
0xfc, 0x02, 0x34, 0x23, 0xe4, 0xce, 0x44, 0xdc, 0x86, 0xd1, 0x9b, 0x06, 0x2d, 0xeb, 0xef, 0x3d,
0x94, 0xca, 0x87, 0x6b, 0xb9, 0xc8, 0x58, 0x53, 0x6d, 0x29, 0x7a, 0x55, 0xa0, 0xa3, 0x25, 0xed,
0xa7, 0x4f, 0x99, 0xf2, 0xd3, 0x87, 0xe8, 0xc8, 0x9a, 0x0d, 0x3b, 0x6b, 0x2a, 0x1d, 0xfd, 0x7f,
0x79, 0x07, 0x0f, 0xf6, 0x49, 0xf9, 0xc5, 0x3f, 0xc9, 0x16, 0x5e, 0xd6, 0xb4, 0xc4, 0x8a, 0x97,
0x87, 0x1b, 0x6a, 0xc5, 0xcb, 0xdf, 0x74, 0xd6, 0xdb, 0xcf, 0xbf, 0x3d, 0x1b, 0x3a, 0x7c, 0x34,
0xbd, 0x3e, 0x1d, 0x78, 0x93, 0xb3, 0xb1, 0x33, 0x1c, 0x71, 0xd7, 0x71, 0x87, 0x2e, 0xe3, 0x3f,
0x7a, 0xc1, 0xed, 0xd9, 0xd8, 0xb5, 0xcf, 0xe4, 0x2b, 0xed, 0x6c, 0x6e, 0xee, 0x3a, 0x25, 0x1f,
0xf8, 0x5f, 0xfc, 0x15, 0x00, 0x00, 0xff, 0xff, 0xe1, 0x3e, 0x2a, 0xde, 0x10, 0x0c, 0x00, 0x00,
} }
// Reference imports to suppress errors if they are not otherwise used. // Reference imports to suppress errors if they are not otherwise used.
@ -860,6 +1149,14 @@ type RouterClient interface {
//differs from SendPayment in that it allows users to specify a full route //differs from SendPayment in that it allows users to specify a full route
//manually. This can be used for things like rebalancing, and atomic swaps. //manually. This can be used for things like rebalancing, and atomic swaps.
SendToRoute(ctx context.Context, in *SendToRouteRequest, opts ...grpc.CallOption) (*SendToRouteResponse, error) SendToRoute(ctx context.Context, in *SendToRouteRequest, opts ...grpc.CallOption) (*SendToRouteResponse, error)
//*
//ResetMissionControl clears all mission control state and starts with a clean
//slate.
ResetMissionControl(ctx context.Context, in *ResetMissionControlRequest, opts ...grpc.CallOption) (*ResetMissionControlResponse, error)
//*
//QueryMissionControl exposes the internal mission control state to callers.
//It is a development feature.
QueryMissionControl(ctx context.Context, in *QueryMissionControlRequest, opts ...grpc.CallOption) (*QueryMissionControlResponse, error)
} }
type routerClient struct { type routerClient struct {
@ -897,6 +1194,24 @@ func (c *routerClient) SendToRoute(ctx context.Context, in *SendToRouteRequest,
return out, nil return out, nil
} }
func (c *routerClient) ResetMissionControl(ctx context.Context, in *ResetMissionControlRequest, opts ...grpc.CallOption) (*ResetMissionControlResponse, error) {
out := new(ResetMissionControlResponse)
err := c.cc.Invoke(ctx, "/routerrpc.Router/ResetMissionControl", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *routerClient) QueryMissionControl(ctx context.Context, in *QueryMissionControlRequest, opts ...grpc.CallOption) (*QueryMissionControlResponse, error) {
out := new(QueryMissionControlResponse)
err := c.cc.Invoke(ctx, "/routerrpc.Router/QueryMissionControl", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// RouterServer is the server API for Router service. // RouterServer is the server API for Router service.
type RouterServer interface { type RouterServer interface {
//* //*
@ -915,6 +1230,14 @@ type RouterServer interface {
//differs from SendPayment in that it allows users to specify a full route //differs from SendPayment in that it allows users to specify a full route
//manually. This can be used for things like rebalancing, and atomic swaps. //manually. This can be used for things like rebalancing, and atomic swaps.
SendToRoute(context.Context, *SendToRouteRequest) (*SendToRouteResponse, error) SendToRoute(context.Context, *SendToRouteRequest) (*SendToRouteResponse, error)
//*
//ResetMissionControl clears all mission control state and starts with a clean
//slate.
ResetMissionControl(context.Context, *ResetMissionControlRequest) (*ResetMissionControlResponse, error)
//*
//QueryMissionControl exposes the internal mission control state to callers.
//It is a development feature.
QueryMissionControl(context.Context, *QueryMissionControlRequest) (*QueryMissionControlResponse, error)
} }
func RegisterRouterServer(s *grpc.Server, srv RouterServer) { func RegisterRouterServer(s *grpc.Server, srv RouterServer) {
@ -975,6 +1298,42 @@ func _Router_SendToRoute_Handler(srv interface{}, ctx context.Context, dec func(
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _Router_ResetMissionControl_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ResetMissionControlRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(RouterServer).ResetMissionControl(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/routerrpc.Router/ResetMissionControl",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(RouterServer).ResetMissionControl(ctx, req.(*ResetMissionControlRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Router_QueryMissionControl_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(QueryMissionControlRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(RouterServer).QueryMissionControl(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/routerrpc.Router/QueryMissionControl",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(RouterServer).QueryMissionControl(ctx, req.(*QueryMissionControlRequest))
}
return interceptor(ctx, in, info, handler)
}
var _Router_serviceDesc = grpc.ServiceDesc{ var _Router_serviceDesc = grpc.ServiceDesc{
ServiceName: "routerrpc.Router", ServiceName: "routerrpc.Router",
HandlerType: (*RouterServer)(nil), HandlerType: (*RouterServer)(nil),
@ -991,6 +1350,14 @@ var _Router_serviceDesc = grpc.ServiceDesc{
MethodName: "SendToRoute", MethodName: "SendToRoute",
Handler: _Router_SendToRoute_Handler, Handler: _Router_SendToRoute_Handler,
}, },
{
MethodName: "ResetMissionControl",
Handler: _Router_ResetMissionControl_Handler,
},
{
MethodName: "QueryMissionControl",
Handler: _Router_QueryMissionControl_Handler,
},
}, },
Streams: []grpc.StreamDesc{}, Streams: []grpc.StreamDesc{},
Metadata: "routerrpc/router.proto", Metadata: "routerrpc/router.proto",

@ -245,6 +245,46 @@ message ChannelUpdate {
*/ */
bytes extra_opaque_data = 12; bytes extra_opaque_data = 12;
} }
message ResetMissionControlRequest{}
message ResetMissionControlResponse{}
message QueryMissionControlRequest {}
/// QueryMissionControlResponse contains mission control state per node.
message QueryMissionControlResponse {
repeated NodeHistory nodes = 1;
}
/// NodeHistory contains the mission control state for a particular node.
message NodeHistory {
/// Node pubkey
bytes pubkey = 1 [json_name = "pubkey"];
/// Time stamp of last failure. Set to zero if no failure happened yet.
int64 last_fail_time = 2 [json_name = "last_fail_time"];
/// Estimation of success probability for channels not in the channel array.
float other_chan_success_prob = 3 [json_name = "other_chan_success_prob"];
/// Historical information of particular channels.
repeated ChannelHistory channels = 4 [json_name = "channels"];
}
/// NodeHistory contains the mission control state for a particular channel.
message ChannelHistory {
/// Short channel id
uint64 channel_id = 1 [json_name = "channel_id"];
/// Time stamp of last failure.
int64 last_fail_time = 2 [json_name = "last_fail_time"];
/// Minimum penalization amount.
int64 min_penalize_amt_sat = 3 [json_name = "min_penalize_amt_sat"];
/// Estimation of success probability for this channel.
float success_prob = 4 [json_name = "success_prob"];
}
service Router { service Router {
/** /**
@ -268,4 +308,16 @@ service Router {
manually. This can be used for things like rebalancing, and atomic swaps. manually. This can be used for things like rebalancing, and atomic swaps.
*/ */
rpc SendToRoute(SendToRouteRequest) returns (SendToRouteResponse); rpc SendToRoute(SendToRouteRequest) returns (SendToRouteResponse);
/**
ResetMissionControl clears all mission control state and starts with a clean
slate.
*/
rpc ResetMissionControl(ResetMissionControlRequest) returns (ResetMissionControlResponse);
/**
QueryMissionControl exposes the internal mission control state to callers.
It is a development feature.
*/
rpc QueryMissionControl(QueryMissionControlRequest) returns (QueryMissionControlResponse);
} }

@ -36,6 +36,8 @@ type RouterBackend struct {
FindRoute func(source, target route.Vertex, FindRoute func(source, target route.Vertex,
amt lnwire.MilliSatoshi, restrictions *routing.RestrictParams, amt lnwire.MilliSatoshi, restrictions *routing.RestrictParams,
finalExpiry ...uint16) (*route.Route, error) finalExpiry ...uint16) (*route.Route, error)
MissionControl *routing.MissionControl
} }
// QueryRoutes attempts to query the daemons' Channel Router for a possible // QueryRoutes attempts to query the daemons' Channel Router for a possible
@ -122,8 +124,21 @@ func (r *RouterBackend) QueryRoutes(ctx context.Context,
restrictions := &routing.RestrictParams{ restrictions := &routing.RestrictParams{
FeeLimit: feeLimit, FeeLimit: feeLimit,
IgnoredNodes: ignoredNodes, ProbabilitySource: func(node route.Vertex,
IgnoredEdges: ignoredEdges, edge routing.EdgeLocator,
amt lnwire.MilliSatoshi) float64 {
if _, ok := ignoredNodes[node]; ok {
return 0
}
if _, ok := ignoredEdges[edge]; ok {
return 0
}
return 1
},
PaymentAttemptPenalty: routing.DefaultPaymentAttemptPenalty,
} }
// Query the channel router for a possible path to the destination that // Query the channel router for a possible path to the destination that

@ -39,6 +39,11 @@ func TestQueryRoutes(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
ignoredEdge := routing.EdgeLocator{
ChannelID: 555,
Direction: 1,
}
request := &lnrpc.QueryRoutesRequest{ request := &lnrpc.QueryRoutesRequest{
PubKey: destKey, PubKey: destKey,
Amt: 100000, Amt: 100000,
@ -75,22 +80,22 @@ func TestQueryRoutes(t *testing.T) {
t.Fatal("unexpected fee limit") t.Fatal("unexpected fee limit")
} }
if len(restrictions.IgnoredEdges) != 1 { if restrictions.ProbabilitySource(route.Vertex{},
t.Fatal("unexpected ignored edges map size") ignoredEdge, 0,
) != 0 {
t.Fatal("expecting 0% probability for ignored edge")
} }
if _, ok := restrictions.IgnoredEdges[routing.EdgeLocator{ if restrictions.ProbabilitySource(ignoreNodeVertex,
ChannelID: 555, Direction: 1, routing.EdgeLocator{}, 0,
}]; !ok { ) != 0 {
t.Fatal("unexpected ignored edge") t.Fatal("expecting 0% probability for ignored node")
} }
if len(restrictions.IgnoredNodes) != 1 { if restrictions.ProbabilitySource(route.Vertex{},
t.Fatal("unexpected ignored nodes map size") routing.EdgeLocator{}, 0,
} ) != 1 {
t.Fatal("expecting 100% probability")
if _, ok := restrictions.IgnoredNodes[ignoreNodeVertex]; !ok {
t.Fatal("unexpected ignored node")
} }
hops := []*route.Hop{{}} hops := []*route.Hop{{}}

@ -59,6 +59,14 @@ var (
Entity: "offchain", Entity: "offchain",
Action: "read", Action: "read",
}}, }},
"/routerrpc.Router/QueryMissionControl": {{
Entity: "offchain",
Action: "read",
}},
"/routerrpc.Router/ResetMissionControl": {{
Entity: "offchain",
Action: "write",
}},
} }
// DefaultRouterMacFilename is the default name of the router macaroon // DefaultRouterMacFilename is the default name of the router macaroon
@ -439,3 +447,56 @@ func marshallChannelUpdate(update *lnwire.ChannelUpdate) *ChannelUpdate {
ExtraOpaqueData: update.ExtraOpaqueData, ExtraOpaqueData: update.ExtraOpaqueData,
} }
} }
// ResetMissionControl clears all mission control state and starts with a clean
// slate.
func (s *Server) ResetMissionControl(ctx context.Context,
req *ResetMissionControlRequest) (*ResetMissionControlResponse, error) {
s.cfg.RouterBackend.MissionControl.ResetHistory()
return &ResetMissionControlResponse{}, nil
}
// QueryMissionControl exposes the internal mission control state to callers. It
// is a development feature.
func (s *Server) QueryMissionControl(ctx context.Context,
req *QueryMissionControlRequest) (*QueryMissionControlResponse, error) {
snapshot := s.cfg.RouterBackend.MissionControl.GetHistorySnapshot()
rpcNodes := make([]*NodeHistory, len(snapshot.Nodes))
for i, node := range snapshot.Nodes {
channels := make([]*ChannelHistory, len(node.Channels))
for j, channel := range node.Channels {
channels[j] = &ChannelHistory{
ChannelId: channel.ChannelID,
LastFailTime: channel.LastFail.Unix(),
MinPenalizeAmtSat: int64(
channel.MinPenalizeAmt.ToSatoshis(),
),
SuccessProb: float32(channel.SuccessProb),
}
}
var lastFail int64
if node.LastFail != nil {
lastFail = node.LastFail.Unix()
}
rpcNodes[i] = &NodeHistory{
Pubkey: node.Node[:],
LastFailTime: lastFail,
OtherChanSuccessProb: float32(
node.OtherChanSuccessProb,
),
Channels: channels,
}
}
response := QueryMissionControlResponse{
Nodes: rpcNodes,
}
return &response, nil
}

@ -32,6 +32,7 @@ import (
"github.com/lightningnetwork/lnd" "github.com/lightningnetwork/lnd"
"github.com/lightningnetwork/lnd/chanbackup" "github.com/lightningnetwork/lnd/chanbackup"
"github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
"github.com/lightningnetwork/lnd/lntest" "github.com/lightningnetwork/lnd/lntest"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
"golang.org/x/net/context" "golang.org/x/net/context"
@ -8382,8 +8383,14 @@ out:
// failed payment. // failed payment.
shutdownAndAssert(net, t, carol) shutdownAndAssert(net, t, carol)
// TODO(roasbeef): mission control // Reset mission control to forget the temporary channel failure above.
time.Sleep(time.Second * 5) ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
_, err = net.Alice.RouterClient.ResetMissionControl(
ctxt, &routerrpc.ResetMissionControlRequest{},
)
if err != nil {
t.Fatalf("unable to reset mission control: %v", err)
}
sendReq = &lnrpc.SendRequest{ sendReq = &lnrpc.SendRequest{
PaymentRequest: carolInvoice.PaymentRequest, PaymentRequest: carolInvoice.PaymentRequest,

@ -28,6 +28,7 @@ import (
"github.com/lightningnetwork/lnd/chanbackup" "github.com/lightningnetwork/lnd/chanbackup"
"github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
"github.com/lightningnetwork/lnd/macaroons" "github.com/lightningnetwork/lnd/macaroons"
) )
@ -248,6 +249,10 @@ type HarnessNode struct {
lnrpc.WalletUnlockerClient lnrpc.WalletUnlockerClient
invoicesrpc.InvoicesClient invoicesrpc.InvoicesClient
// RouterClient cannot be embedded, because a name collision would occur
// on the main rpc SendPayment.
RouterClient routerrpc.RouterClient
} }
// Assert *HarnessNode implements the lnrpc.LightningClient interface. // Assert *HarnessNode implements the lnrpc.LightningClient interface.
@ -497,6 +502,7 @@ func (hn *HarnessNode) initLightningClient(conn *grpc.ClientConn) error {
// HarnessNode directly for normal rpc operations. // HarnessNode directly for normal rpc operations.
hn.LightningClient = lnrpc.NewLightningClient(conn) hn.LightningClient = lnrpc.NewLightningClient(conn)
hn.InvoicesClient = invoicesrpc.NewInvoicesClient(conn) hn.InvoicesClient = invoicesrpc.NewInvoicesClient(conn)
hn.RouterClient = routerrpc.NewRouterClient(conn)
// Set the harness node's pubkey to what the node claims in GetInfo. // Set the harness node's pubkey to what the node claims in GetInfo.
err := hn.FetchNodeInfo() err := hn.FetchNodeInfo()

@ -25,8 +25,14 @@ type nodeWithDist struct {
// node. This value does not include the final cltv. // node. This value does not include the final cltv.
incomingCltv uint32 incomingCltv uint32
// fee is the fee that this node is charging for forwarding. // probability is the probability that from this node onward the route
fee lnwire.MilliSatoshi // is successful.
probability float64
// weight is the cost of the route from this node to the destination.
// Includes the routing fees and a virtual cost factor to account for
// time locks.
weight int64
} }
// distanceHeap is a min-distance heap that's used within our path finding // distanceHeap is a min-distance heap that's used within our path finding

@ -1,6 +1,7 @@
package routing package routing
import ( import (
"math"
"sync" "sync"
"time" "time"
@ -13,48 +14,23 @@ import (
) )
const ( const (
// vertexDecay is the decay period of colored vertexes added to // DefaultPenaltyHalfLife is the default half-life duration. The
// MissionControl. Once vertexDecay passes after an entry has been // half-life duration defines after how much time a penalized node or
// added to the prune view, it is garbage collected. This value is // channel is back at 50% probability.
// larger than edgeDecay as an edge failure typical indicates an DefaultPenaltyHalfLife = time.Hour
// unbalanced channel, while a vertex failure indicates a node is not
// online and active.
vertexDecay = time.Duration(time.Minute * 5)
// edgeDecay is the decay period of colored edges added to
// MissionControl. Once edgeDecay passed after an entry has been added,
// it is garbage collected. This value is smaller than vertexDecay as
// an edge related failure during payment sending typically indicates
// that a channel was unbalanced, a condition which may quickly change.
//
// TODO(roasbeef): instead use random delay on each?
edgeDecay = time.Duration(time.Second * 5)
) )
// MissionControl contains state which summarizes the past attempts of HTLC // MissionControl contains state which summarizes the past attempts of HTLC
// routing by external callers when sending payments throughout the network. // routing by external callers when sending payments throughout the network. It
// MissionControl remembers the outcome of these past routing attempts (success // acts as a shared memory during routing attempts with the goal to optimize the
// and failure), and is able to provide hints/guidance to future HTLC routing // payment attempt success rate.
// attempts. MissionControl maintains a decaying network view of the //
// edges/vertexes that should be marked as "pruned" during path finding. This // Failed payment attempts are reported to mission control. These reports are
// graph view acts as a shared memory during HTLC payment routing attempts. // used to track the time of the last node or channel level failure. The time
// With each execution, if an error is encountered, based on the type of error // since the last failure is used to estimate a success probability that is fed
// and the location of the error within the route, an edge or vertex is added // into the path finding process for subsequent payment attempts.
// to the view. Later sending attempts will then query the view for all the
// vertexes/edges that should be ignored. Items in the view decay after a set
// period of time, allowing the view to be dynamic w.r.t network changes.
type MissionControl struct { type MissionControl struct {
// failedEdges maps a short channel ID to be pruned, to the time that history map[route.Vertex]*nodeHistory
// it was added to the prune view. Edges are added to this map if a
// caller reports to MissionControl a failure localized to that edge
// when sending a payment.
failedEdges map[EdgeLocator]time.Time
// failedVertexes maps a node's public key that should be pruned, to
// the time that it was added to the prune view. Vertexes are added to
// this map if a caller reports to MissionControl a failure localized
// to that particular vertex.
failedVertexes map[route.Vertex]time.Time
graph *channeldb.ChannelGraph graph *channeldb.ChannelGraph
@ -62,6 +38,12 @@ type MissionControl struct {
queryBandwidth func(*channeldb.ChannelEdgeInfo) lnwire.MilliSatoshi queryBandwidth func(*channeldb.ChannelEdgeInfo) lnwire.MilliSatoshi
// now is expected to return the current time. It is supplied as an
// external function to enable deterministic unit tests.
now func() time.Time
cfg *MissionControlConfig
sync.Mutex sync.Mutex
// TODO(roasbeef): further counters, if vertex continually unavailable, // TODO(roasbeef): further counters, if vertex continually unavailable,
@ -74,83 +56,113 @@ type MissionControl struct {
// PaymentSessionSource interface. // PaymentSessionSource interface.
var _ PaymentSessionSource = (*MissionControl)(nil) var _ PaymentSessionSource = (*MissionControl)(nil)
// NewMissionControl returns a new instance of MissionControl. // MissionControlConfig defines parameters that control mission control
// behaviour.
type MissionControlConfig struct {
// PenaltyHalfLife defines after how much time a penalized node or
// channel is back at 50% probability.
PenaltyHalfLife time.Duration
// PaymentAttemptPenalty is the virtual cost in path finding weight
// units of executing a payment attempt that fails. It is used to trade
// off potentially better routes against their probability of
// succeeding.
PaymentAttemptPenalty lnwire.MilliSatoshi
// MinProbability defines the minimum success probability of the
// returned route.
MinRouteProbability float64
// AprioriHopProbability is the assumed success probability of a hop in
// a route when no other information is available.
AprioriHopProbability float64
}
// nodeHistory contains a summary of payment attempt outcomes involving a
// particular node.
type nodeHistory struct {
// lastFail is the last time a node level failure occurred, if any.
lastFail *time.Time
// channelLastFail tracks history per channel, if available for that
// channel.
channelLastFail map[uint64]*channelHistory
}
// channelHistory contains a summary of payment attempt outcomes involving a
// particular channel.
type channelHistory struct {
// lastFail is the last time a channel level failure occurred.
lastFail time.Time
// minPenalizeAmt is the minimum amount for which to take this failure
// into account.
minPenalizeAmt lnwire.MilliSatoshi
}
// MissionControlSnapshot contains a snapshot of the current state of mission
// control.
type MissionControlSnapshot struct {
// Nodes contains the per node information of this snapshot.
Nodes []MissionControlNodeSnapshot
}
// MissionControlNodeSnapshot contains a snapshot of the current node state in
// mission control.
type MissionControlNodeSnapshot struct {
// Node pubkey.
Node route.Vertex
// Lastfail is the time of last failure, if any.
LastFail *time.Time
// Channels is a list of channels for which specific information is
// logged.
Channels []MissionControlChannelSnapshot
// OtherChanSuccessProb is the success probability for channels not in
// the Channels slice.
OtherChanSuccessProb float64
}
// MissionControlChannelSnapshot contains a snapshot of the current channel
// state in mission control.
type MissionControlChannelSnapshot struct {
// ChannelID is the short channel id of the snapshot.
ChannelID uint64
// LastFail is the time of last failure.
LastFail time.Time
// MinPenalizeAmt is the minimum amount for which the channel will be
// penalized.
MinPenalizeAmt lnwire.MilliSatoshi
// SuccessProb is the success probability estimation for this channel.
SuccessProb float64
}
// NewMissionControl returns a new instance of missionControl.
// //
// TODO(roasbeef): persist memory // TODO(roasbeef): persist memory
func NewMissionControl(g *channeldb.ChannelGraph, selfNode *channeldb.LightningNode, func NewMissionControl(g *channeldb.ChannelGraph, selfNode *channeldb.LightningNode,
qb func(*channeldb.ChannelEdgeInfo) lnwire.MilliSatoshi) *MissionControl { qb func(*channeldb.ChannelEdgeInfo) lnwire.MilliSatoshi,
cfg *MissionControlConfig) *MissionControl {
log.Debugf("Instantiating mission control with config: "+
"PenaltyHalfLife=%v, PaymentAttemptPenalty=%v, "+
"MinRouteProbability=%v, AprioriHopProbability=%v",
cfg.PenaltyHalfLife,
int64(cfg.PaymentAttemptPenalty.ToSatoshis()),
cfg.MinRouteProbability, cfg.AprioriHopProbability)
return &MissionControl{ return &MissionControl{
failedEdges: make(map[EdgeLocator]time.Time), history: make(map[route.Vertex]*nodeHistory),
failedVertexes: make(map[route.Vertex]time.Time),
selfNode: selfNode, selfNode: selfNode,
queryBandwidth: qb, queryBandwidth: qb,
graph: g, graph: g,
} now: time.Now,
} cfg: cfg,
// graphPruneView is a filter of sorts that path finding routines should
// consult during the execution. Any edges or vertexes within the view should
// be ignored during path finding. The contents of the view reflect the current
// state of the wider network from the PoV of mission control compiled via HTLC
// routing attempts in the past.
type graphPruneView struct {
edges map[EdgeLocator]struct{}
vertexes map[route.Vertex]struct{}
}
// graphPruneView returns a new graphPruneView instance which is to be
// consulted during path finding. If a vertex/edge is found within the returned
// prune view, it is to be ignored as a goroutine has had issues routing
// through it successfully. Within this method the main view of the
// MissionControl is garbage collected as entries are detected to be "stale".
func (m *MissionControl) graphPruneView() graphPruneView {
// First, we'll grab the current time, this value will be used to
// determine if an entry is stale or not.
now := time.Now()
m.Lock()
// For each of the vertexes that have been added to the prune view, if
// it is now "stale", then we'll ignore it and avoid adding it to the
// view we'll return.
vertexes := make(map[route.Vertex]struct{})
for vertex, pruneTime := range m.failedVertexes {
if now.Sub(pruneTime) >= vertexDecay {
log.Tracef("Pruning decayed failure report for vertex %v "+
"from Mission Control", vertex)
delete(m.failedVertexes, vertex)
continue
}
vertexes[vertex] = struct{}{}
}
// We'll also do the same for edges, but use the edgeDecay this time
// rather than the decay for vertexes.
edges := make(map[EdgeLocator]struct{})
for edge, pruneTime := range m.failedEdges {
if now.Sub(pruneTime) >= edgeDecay {
log.Tracef("Pruning decayed failure report for edge %v "+
"from Mission Control", edge)
delete(m.failedEdges, edge)
continue
}
edges[edge] = struct{}{}
}
m.Unlock()
log.Debugf("Mission Control returning prune view of %v edges, %v "+
"vertexes", len(edges), len(vertexes))
return graphPruneView{
edges: edges,
vertexes: vertexes,
} }
} }
@ -161,8 +173,6 @@ func (m *MissionControl) graphPruneView() graphPruneView {
func (m *MissionControl) NewPaymentSession(routeHints [][]zpay32.HopHint, func (m *MissionControl) NewPaymentSession(routeHints [][]zpay32.HopHint,
target route.Vertex) (PaymentSession, error) { target route.Vertex) (PaymentSession, error) {
viewSnapshot := m.graphPruneView()
edges := make(map[route.Vertex][]*channeldb.ChannelEdgePolicy) edges := make(map[route.Vertex][]*channeldb.ChannelEdgePolicy)
// Traverse through all of the available hop hints and include them in // Traverse through all of the available hop hints and include them in
@ -226,10 +236,9 @@ func (m *MissionControl) NewPaymentSession(routeHints [][]zpay32.HopHint,
} }
return &paymentSession{ return &paymentSession{
pruneViewSnapshot: viewSnapshot,
additionalEdges: edges, additionalEdges: edges,
bandwidthHints: bandwidthHints, bandwidthHints: bandwidthHints,
errFailedPolicyChans: make(map[EdgeLocator]struct{}), errFailedPolicyChans: make(map[nodeChannel]struct{}),
mc: m, mc: m,
pathFinder: findPath, pathFinder: findPath,
}, nil }, nil
@ -239,8 +248,7 @@ func (m *MissionControl) NewPaymentSession(routeHints [][]zpay32.HopHint,
// used for failure reporting to missioncontrol. // used for failure reporting to missioncontrol.
func (m *MissionControl) NewPaymentSessionForRoute(preBuiltRoute *route.Route) PaymentSession { func (m *MissionControl) NewPaymentSessionForRoute(preBuiltRoute *route.Route) PaymentSession {
return &paymentSession{ return &paymentSession{
pruneViewSnapshot: m.graphPruneView(), errFailedPolicyChans: make(map[nodeChannel]struct{}),
errFailedPolicyChans: make(map[EdgeLocator]struct{}),
mc: m, mc: m,
preBuiltRoute: preBuiltRoute, preBuiltRoute: preBuiltRoute,
} }
@ -251,8 +259,7 @@ func (m *MissionControl) NewPaymentSessionForRoute(preBuiltRoute *route.Route) P
// missioncontrol for resumed payment we don't want to make more attempts for. // missioncontrol for resumed payment we don't want to make more attempts for.
func (m *MissionControl) NewPaymentSessionEmpty() PaymentSession { func (m *MissionControl) NewPaymentSessionEmpty() PaymentSession {
return &paymentSession{ return &paymentSession{
pruneViewSnapshot: m.graphPruneView(), errFailedPolicyChans: make(map[nodeChannel]struct{}),
errFailedPolicyChans: make(map[EdgeLocator]struct{}),
mc: m, mc: m,
preBuiltRoute: &route.Route{}, preBuiltRoute: &route.Route{},
preBuiltRouteTried: true, preBuiltRouteTried: true,
@ -298,7 +305,174 @@ func generateBandwidthHints(sourceNode *channeldb.LightningNode,
// if no payment attempts have been made. // if no payment attempts have been made.
func (m *MissionControl) ResetHistory() { func (m *MissionControl) ResetHistory() {
m.Lock() m.Lock()
m.failedEdges = make(map[EdgeLocator]time.Time) defer m.Unlock()
m.failedVertexes = make(map[route.Vertex]time.Time)
m.Unlock() m.history = make(map[route.Vertex]*nodeHistory)
log.Debugf("Mission control history cleared")
}
// getEdgeProbability is expected to return the success probability of a payment
// from fromNode along edge.
func (m *MissionControl) getEdgeProbability(fromNode route.Vertex,
edge EdgeLocator, amt lnwire.MilliSatoshi) float64 {
m.Lock()
defer m.Unlock()
// Get the history for this node. If there is no history available,
// assume that it's success probability is a constant a priori
// probability. After the attempt new information becomes available to
// adjust this probability.
nodeHistory, ok := m.history[fromNode]
if !ok {
return m.cfg.AprioriHopProbability
}
return m.getEdgeProbabilityForNode(nodeHistory, edge.ChannelID, amt)
}
// getEdgeProbabilityForNode estimates the probability of successfully
// traversing a channel based on the node history.
func (m *MissionControl) getEdgeProbabilityForNode(nodeHistory *nodeHistory,
channelID uint64, amt lnwire.MilliSatoshi) float64 {
// Calculate the last failure of the given edge. A node failure is
// considered a failure that would have affected every edge. Therefore
// we insert a node level failure into the history of every channel.
lastFailure := nodeHistory.lastFail
// Take into account a minimum penalize amount. For balance errors, a
// failure may be reported with such a minimum to prevent too aggresive
// penalization. We only take into account a previous failure if the
// amount that we currently get the probability for is greater or equal
// than the minPenalizeAmt of the previous failure.
channelHistory, ok := nodeHistory.channelLastFail[channelID]
if ok && channelHistory.minPenalizeAmt <= amt {
// If there is both a node level failure recorded and a channel
// level failure is applicable too, we take the most recent of
// the two.
if lastFailure == nil ||
channelHistory.lastFail.After(*lastFailure) {
lastFailure = &channelHistory.lastFail
}
}
if lastFailure == nil {
return m.cfg.AprioriHopProbability
}
timeSinceLastFailure := m.now().Sub(*lastFailure)
// Calculate success probability. It is an exponential curve that brings
// the probability down to zero when a failure occurs. From there it
// recovers asymptotically back to the a priori probability. The rate at
// which this happens is controlled by the penaltyHalfLife parameter.
exp := -timeSinceLastFailure.Hours() / m.cfg.PenaltyHalfLife.Hours()
probability := m.cfg.AprioriHopProbability * (1 - math.Pow(2, exp))
return probability
}
// createHistoryIfNotExists returns the history for the given node. If the node
// is yet unknown, it will create an empty history structure.
func (m *MissionControl) createHistoryIfNotExists(vertex route.Vertex) *nodeHistory {
if node, ok := m.history[vertex]; ok {
return node
}
node := &nodeHistory{
channelLastFail: make(map[uint64]*channelHistory),
}
m.history[vertex] = node
return node
}
// reportVertexFailure reports a node level failure.
func (m *MissionControl) reportVertexFailure(v route.Vertex) {
log.Debugf("Reporting vertex %v failure to Mission Control", v)
now := m.now()
m.Lock()
defer m.Unlock()
history := m.createHistoryIfNotExists(v)
history.lastFail = &now
}
// reportEdgeFailure reports a channel level failure.
//
// TODO(roasbeef): also add value attempted to send and capacity of channel
func (m *MissionControl) reportEdgeFailure(failedEdge edge,
minPenalizeAmt lnwire.MilliSatoshi) {
log.Debugf("Reporting channel %v failure to Mission Control",
failedEdge.channel)
now := m.now()
m.Lock()
defer m.Unlock()
history := m.createHistoryIfNotExists(failedEdge.from)
history.channelLastFail[failedEdge.channel] = &channelHistory{
lastFail: now,
minPenalizeAmt: minPenalizeAmt,
}
}
// GetHistorySnapshot takes a snapshot from the current mission control state
// and actual probability estimates.
func (m *MissionControl) GetHistorySnapshot() *MissionControlSnapshot {
m.Lock()
defer m.Unlock()
log.Debugf("Requesting history snapshot from mission control: "+
"node_count=%v", len(m.history))
nodes := make([]MissionControlNodeSnapshot, 0, len(m.history))
for v, h := range m.history {
channelSnapshot := make([]MissionControlChannelSnapshot, 0,
len(h.channelLastFail),
)
for id, lastFail := range h.channelLastFail {
// Show probability assuming amount meets min
// penalization amount.
prob := m.getEdgeProbabilityForNode(
h, id, lastFail.minPenalizeAmt,
)
channelSnapshot = append(channelSnapshot,
MissionControlChannelSnapshot{
ChannelID: id,
LastFail: lastFail.lastFail,
MinPenalizeAmt: lastFail.minPenalizeAmt,
SuccessProb: prob,
},
)
}
otherProb := m.getEdgeProbabilityForNode(h, 0, 0)
nodes = append(nodes,
MissionControlNodeSnapshot{
Node: v,
LastFail: h.lastFail,
OtherChanSuccessProb: otherProb,
Channels: channelSnapshot,
},
)
}
snapshot := MissionControlSnapshot{
Nodes: nodes,
}
return &snapshot
} }

@ -0,0 +1,81 @@
package routing
import (
"testing"
"time"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing/route"
)
// TestMissionControl tests mission control probability estimation.
func TestMissionControl(t *testing.T) {
now := testTime
mc := NewMissionControl(
nil, nil, nil, &MissionControlConfig{
PenaltyHalfLife: 30 * time.Minute,
AprioriHopProbability: 0.8,
},
)
mc.now = func() time.Time { return now }
testTime := time.Date(2018, time.January, 9, 14, 00, 00, 0, time.UTC)
testNode := route.Vertex{}
testEdge := edge{
channel: 123,
}
expectP := func(amt lnwire.MilliSatoshi, expected float64) {
t.Helper()
p := mc.getEdgeProbability(
testNode, EdgeLocator{ChannelID: testEdge.channel},
amt,
)
if p != expected {
t.Fatalf("unexpected probability %v", p)
}
}
// Initial probability is expected to be 1.
expectP(1000, 0.8)
// Expect probability to be zero after reporting the edge as failed.
mc.reportEdgeFailure(testEdge, 1000)
expectP(1000, 0)
// As we reported with a min penalization amt, a lower amt than reported
// should be unaffected.
expectP(500, 0.8)
// Edge decay started.
now = testTime.Add(30 * time.Minute)
expectP(1000, 0.4)
// Edge fails again, this time without a min penalization amt. The edge
// should be penalized regardless of amount.
mc.reportEdgeFailure(testEdge, 0)
expectP(1000, 0)
expectP(500, 0)
// Edge decay started.
now = testTime.Add(60 * time.Minute)
expectP(1000, 0.4)
// A node level failure should bring probability of every channel back
// to zero.
mc.reportVertexFailure(testNode)
expectP(1000, 0)
// Check whether history snapshot looks sane.
history := mc.GetHistorySnapshot()
if len(history.Nodes) != 1 {
t.Fatal("unexpected number of nodes")
}
if len(history.Nodes[0].Channels) != 1 {
t.Fatal("unexpected number of channels")
}
}

@ -112,10 +112,9 @@ func (m *mockPaymentSession) RequestRoute(payment *LightningPayment,
func (m *mockPaymentSession) ReportVertexFailure(v route.Vertex) {} func (m *mockPaymentSession) ReportVertexFailure(v route.Vertex) {}
func (m *mockPaymentSession) ReportEdgeFailure(e *EdgeLocator) {} func (m *mockPaymentSession) ReportEdgeFailure(failedEdge edge, minPenalizeAmt lnwire.MilliSatoshi) {}
func (m *mockPaymentSession) ReportEdgePolicyFailure(errSource route.Vertex, failedEdge *EdgeLocator) { func (m *mockPaymentSession) ReportEdgePolicyFailure(failedEdge edge) {}
}
type mockPayer struct { type mockPayer struct {
sendResult chan error sendResult chan error

@ -40,6 +40,22 @@ type pathFinder = func(g *graphParams, r *RestrictParams,
source, target route.Vertex, amt lnwire.MilliSatoshi) ( source, target route.Vertex, amt lnwire.MilliSatoshi) (
[]*channeldb.ChannelEdgePolicy, error) []*channeldb.ChannelEdgePolicy, error)
var (
// DefaultPaymentAttemptPenalty is the virtual cost in path finding weight
// units of executing a payment attempt that fails. It is used to trade
// off potentially better routes against their probability of
// succeeding.
DefaultPaymentAttemptPenalty = lnwire.NewMSatFromSatoshis(100)
// DefaultMinRouteProbability is the default minimum probability for routes
// returned from findPath.
DefaultMinRouteProbability = float64(0.01)
// DefaultAprioriHopProbability is the default a priori probability for
// a hop.
DefaultAprioriHopProbability = float64(0.95)
)
// edgePolicyWithSource is a helper struct to keep track of the source node // edgePolicyWithSource is a helper struct to keep track of the source node
// of a channel edge. ChannelEdgePolicy only contains to destination node // of a channel edge. ChannelEdgePolicy only contains to destination node
// of the edge. // of the edge.
@ -228,13 +244,10 @@ type graphParams struct {
// RestrictParams wraps the set of restrictions passed to findPath that the // RestrictParams wraps the set of restrictions passed to findPath that the
// found path must adhere to. // found path must adhere to.
type RestrictParams struct { type RestrictParams struct {
// IgnoredNodes is an optional set of nodes that should be ignored if // ProbabilitySource is a callback that is expected to return the
// encountered during path finding. // success probability of traversing the channel from the node.
IgnoredNodes map[route.Vertex]struct{} ProbabilitySource func(route.Vertex, EdgeLocator,
lnwire.MilliSatoshi) float64
// IgnoredEdges is an optional set of edges that should be ignored if
// encountered during path finding.
IgnoredEdges map[EdgeLocator]struct{}
// FeeLimit is a maximum fee amount allowed to be used on the path from // FeeLimit is a maximum fee amount allowed to be used on the path from
// the source to the target. // the source to the target.
@ -248,6 +261,16 @@ type RestrictParams struct {
// ctlv. After path finding is complete, the caller needs to increase // ctlv. After path finding is complete, the caller needs to increase
// all cltv expiry heights with the required final cltv delta. // all cltv expiry heights with the required final cltv delta.
CltvLimit *uint32 CltvLimit *uint32
// PaymentAttemptPenalty is the virtual cost in path finding weight
// units of executing a payment attempt that fails. It is used to trade
// off potentially better routes against their probability of
// succeeding.
PaymentAttemptPenalty lnwire.MilliSatoshi
// MinProbability defines the minimum success probability of the
// returned route.
MinProbability float64
} }
// findPath attempts to find a path from the source node within the // findPath attempts to find a path from the source node within the
@ -331,10 +354,11 @@ func findPath(g *graphParams, r *RestrictParams, source, target route.Vertex,
targetNode := &channeldb.LightningNode{PubKeyBytes: target} targetNode := &channeldb.LightningNode{PubKeyBytes: target}
distance[target] = nodeWithDist{ distance[target] = nodeWithDist{
dist: 0, dist: 0,
weight: 0,
node: targetNode, node: targetNode,
amountToReceive: amt, amountToReceive: amt,
fee: 0,
incomingCltv: 0, incomingCltv: 0,
probability: 1,
} }
// We'll use this map as a series of "next" hop pointers. So to get // We'll use this map as a series of "next" hop pointers. So to get
@ -342,15 +366,6 @@ func findPath(g *graphParams, r *RestrictParams, source, target route.Vertex,
// mapped to within `next`. // mapped to within `next`.
next := make(map[route.Vertex]*channeldb.ChannelEdgePolicy) next := make(map[route.Vertex]*channeldb.ChannelEdgePolicy)
ignoredEdges := r.IgnoredEdges
if ignoredEdges == nil {
ignoredEdges = make(map[EdgeLocator]struct{})
}
ignoredNodes := r.IgnoredNodes
if ignoredNodes == nil {
ignoredNodes = make(map[route.Vertex]struct{})
}
// processEdge is a helper closure that will be used to make sure edges // processEdge is a helper closure that will be used to make sure edges
// satisfy our specific requirements. // satisfy our specific requirements.
processEdge := func(fromNode *channeldb.LightningNode, processEdge := func(fromNode *channeldb.LightningNode,
@ -380,21 +395,26 @@ func findPath(g *graphParams, r *RestrictParams, source, target route.Vertex,
return return
} }
// If this vertex or edge has been black listed, then we'll // Calculate amount that the candidate node would have to sent
// skip exploring this edge. // out.
if _, ok := ignoredNodes[fromVertex]; ok {
return
}
locator := newEdgeLocator(edge)
if _, ok := ignoredEdges[*locator]; ok {
return
}
toNodeDist := distance[toNode] toNodeDist := distance[toNode]
amountToSend := toNodeDist.amountToReceive amountToSend := toNodeDist.amountToReceive
// Request the success probability for this edge.
locator := newEdgeLocator(edge)
edgeProbability := r.ProbabilitySource(
fromVertex, *locator, amountToSend,
)
log.Tracef("path finding probability: fromnode=%v, chanid=%v, "+
"probability=%v", fromVertex, locator.ChannelID,
edgeProbability)
// If the probability is zero, there is no point in trying.
if edgeProbability == 0 {
return
}
// If the estimated bandwidth of the channel edge is not able // If the estimated bandwidth of the channel edge is not able
// to carry the amount that needs to be send, return. // to carry the amount that needs to be send, return.
if bandwidth < amountToSend { if bandwidth < amountToSend {
@ -453,19 +473,39 @@ func findPath(g *graphParams, r *RestrictParams, source, target route.Vertex,
return return
} }
// Calculate total probability of successfully reaching target
// by multiplying the probabilities. Both this edge and the rest
// of the route must succeed.
probability := toNodeDist.probability * edgeProbability
// If the probability is below the specified lower bound, we can
// abandon this direction. Adding further nodes can only lower
// the probability more.
if probability < r.MinProbability {
return
}
// By adding fromNode in the route, there will be an extra // By adding fromNode in the route, there will be an extra
// weight composed of the fee that this node will charge and // weight composed of the fee that this node will charge and
// the amount that will be locked for timeLockDelta blocks in // the amount that will be locked for timeLockDelta blocks in
// the HTLC that is handed out to fromNode. // the HTLC that is handed out to fromNode.
weight := edgeWeight(amountToReceive, fee, timeLockDelta) weight := edgeWeight(amountToReceive, fee, timeLockDelta)
// Compute the tentative distance to this new channel/edge // Compute the tentative weight to this new channel/edge
// which is the distance from our toNode to the target node // which is the weight from our toNode to the target node
// plus the weight of this edge. // plus the weight of this edge.
tempDist := toNodeDist.dist + weight tempWeight := toNodeDist.weight + weight
// If this new tentative distance is not better than the current // Add an extra factor to the weight to take into account the
// best known distance to this node, return. // probability.
tempDist := getProbabilityBasedDist(
tempWeight, probability, int64(r.PaymentAttemptPenalty),
)
// If the current best route is better than this candidate
// route, return. It is important to also return if the distance
// is equal, because otherwise the algorithm could run into an
// endless loop.
if tempDist >= distance[fromVertex].dist { if tempDist >= distance[fromVertex].dist {
return return
} }
@ -483,10 +523,11 @@ func findPath(g *graphParams, r *RestrictParams, source, target route.Vertex,
// map is populated with this edge. // map is populated with this edge.
distance[fromVertex] = nodeWithDist{ distance[fromVertex] = nodeWithDist{
dist: tempDist, dist: tempDist,
weight: tempWeight,
node: fromNode, node: fromNode,
amountToReceive: amountToReceive, amountToReceive: amountToReceive,
fee: fee,
incomingCltv: incomingCltv, incomingCltv: incomingCltv,
probability: probability,
} }
next[fromVertex] = edge next[fromVertex] = edge
@ -614,5 +655,53 @@ func findPath(g *graphParams, r *RestrictParams, source, target route.Vertex,
"too many hops") "too many hops")
} }
log.Debugf("Found route: probability=%v, hops=%v, fee=%v\n",
distance[source].probability, numEdges,
distance[source].amountToReceive-amt)
return pathEdges, nil return pathEdges, nil
} }
// getProbabilityBasedDist converts a weight into a distance that takes into
// account the success probability and the (virtual) cost of a failed payment
// attempt.
//
// Derivation:
//
// Suppose there are two routes A and B with fees Fa and Fb and success
// probabilities Pa and Pb.
//
// Is the expected cost of trying route A first and then B lower than trying the
// other way around?
//
// The expected cost of A-then-B is: Pa*Fa + (1-Pa)*Pb*(c+Fb)
//
// The expected cost of B-then-A is: Pb*Fb + (1-Pb)*Pa*(c+Fa)
//
// In these equations, the term representing the case where both A and B fail is
// left out because its value would be the same in both cases.
//
// Pa*Fa + (1-Pa)*Pb*(c+Fb) < Pb*Fb + (1-Pb)*Pa*(c+Fa)
//
// Pa*Fa + Pb*c + Pb*Fb - Pa*Pb*c - Pa*Pb*Fb < Pb*Fb + Pa*c + Pa*Fa - Pa*Pb*c - Pa*Pb*Fa
//
// Removing terms that cancel out:
// Pb*c - Pa*Pb*Fb < Pa*c - Pa*Pb*Fa
//
// Divide by Pa*Pb:
// c/Pa - Fb < c/Pb - Fa
//
// Move terms around:
// Fa + c/Pa < Fb + c/Pb
//
// So the value of F + c/P can be used to compare routes.
func getProbabilityBasedDist(weight int64, probability float64, penalty int64) int64 {
// Clamp probability to prevent overflow.
const minProbability = 0.00001
if probability < minProbability {
return infinity
}
return weight + int64(float64(penalty)/probability)
}

@ -53,6 +53,7 @@ const (
var ( var (
noRestrictions = &RestrictParams{ noRestrictions = &RestrictParams{
FeeLimit: noFeeLimit, FeeLimit: noFeeLimit,
ProbabilitySource: noProbabilitySource,
} }
) )
@ -72,6 +73,12 @@ var (
} }
) )
// noProbabilitySource is used in testing to return the same probability 1 for
// all edges.
func noProbabilitySource(route.Vertex, EdgeLocator, lnwire.MilliSatoshi) float64 {
return 1
}
// testGraph is the struct which corresponds to the JSON format used to encode // testGraph is the struct which corresponds to the JSON format used to encode
// graphs within the files in the testdata directory. // graphs within the files in the testdata directory.
// //
@ -477,6 +484,16 @@ func createTestGraphFromChannels(testChannels []*testChannel) (*testGraphInstanc
Index: 0, Index: 0,
} }
// Sort nodes
node1 := testChannel.Node1
node2 := testChannel.Node2
node1Vertex := aliasMap[node1.Alias]
node2Vertex := aliasMap[node2.Alias]
if bytes.Compare(node1Vertex[:], node2Vertex[:]) == 1 {
node1, node2 = node2, node1
node1Vertex, node2Vertex = node2Vertex, node1Vertex
}
// We first insert the existence of the edge between the two // We first insert the existence of the edge between the two
// nodes. // nodes.
edgeInfo := channeldb.ChannelEdgeInfo{ edgeInfo := channeldb.ChannelEdgeInfo{
@ -485,10 +502,10 @@ func createTestGraphFromChannels(testChannels []*testChannel) (*testGraphInstanc
ChannelPoint: *fundingPoint, ChannelPoint: *fundingPoint,
Capacity: testChannel.Capacity, Capacity: testChannel.Capacity,
NodeKey1Bytes: aliasMap[testChannel.Node1.Alias], NodeKey1Bytes: node1Vertex,
BitcoinKey1Bytes: aliasMap[testChannel.Node1.Alias], BitcoinKey1Bytes: node1Vertex,
NodeKey2Bytes: aliasMap[testChannel.Node2.Alias], NodeKey2Bytes: node2Vertex,
BitcoinKey2Bytes: aliasMap[testChannel.Node2.Alias], BitcoinKey2Bytes: node2Vertex,
} }
err = graph.AddChannelEdge(&edgeInfo) err = graph.AddChannelEdge(&edgeInfo)
@ -510,12 +527,12 @@ func createTestGraphFromChannels(testChannels []*testChannel) (*testGraphInstanc
MessageFlags: msgFlags, MessageFlags: msgFlags,
ChannelFlags: channelFlags, ChannelFlags: channelFlags,
ChannelID: channelID, ChannelID: channelID,
LastUpdate: testChannel.Node1.LastUpdate, LastUpdate: node1.LastUpdate,
TimeLockDelta: testChannel.Node1.Expiry, TimeLockDelta: node1.Expiry,
MinHTLC: testChannel.Node1.MinHTLC, MinHTLC: node1.MinHTLC,
MaxHTLC: testChannel.Node1.MaxHTLC, MaxHTLC: node1.MaxHTLC,
FeeBaseMSat: testChannel.Node1.FeeBaseMsat, FeeBaseMSat: node1.FeeBaseMsat,
FeeProportionalMillionths: testChannel.Node1.FeeRate, FeeProportionalMillionths: node1.FeeRate,
} }
if err := graph.UpdateEdgePolicy(edgePolicy); err != nil { if err := graph.UpdateEdgePolicy(edgePolicy); err != nil {
return nil, err return nil, err
@ -536,12 +553,12 @@ func createTestGraphFromChannels(testChannels []*testChannel) (*testGraphInstanc
MessageFlags: msgFlags, MessageFlags: msgFlags,
ChannelFlags: channelFlags, ChannelFlags: channelFlags,
ChannelID: channelID, ChannelID: channelID,
LastUpdate: testChannel.Node2.LastUpdate, LastUpdate: node2.LastUpdate,
TimeLockDelta: testChannel.Node2.Expiry, TimeLockDelta: node2.Expiry,
MinHTLC: testChannel.Node2.MinHTLC, MinHTLC: node2.MinHTLC,
MaxHTLC: testChannel.Node2.MaxHTLC, MaxHTLC: node2.MaxHTLC,
FeeBaseMSat: testChannel.Node2.FeeBaseMsat, FeeBaseMSat: node2.FeeBaseMsat,
FeeProportionalMillionths: testChannel.Node2.FeeRate, FeeProportionalMillionths: node2.FeeRate,
} }
if err := graph.UpdateEdgePolicy(edgePolicy); err != nil { if err := graph.UpdateEdgePolicy(edgePolicy); err != nil {
return nil, err return nil, err
@ -625,9 +642,7 @@ func TestFindLowestFeePath(t *testing.T) {
&graphParams{ &graphParams{
graph: testGraphInstance.graph, graph: testGraphInstance.graph,
}, },
&RestrictParams{ noRestrictions,
FeeLimit: noFeeLimit,
},
sourceNode.PubKeyBytes, target, paymentAmt, sourceNode.PubKeyBytes, target, paymentAmt,
) )
if err != nil { if err != nil {
@ -767,6 +782,7 @@ func testBasicGraphPathFindingCase(t *testing.T, graphInstance *testGraphInstanc
}, },
&RestrictParams{ &RestrictParams{
FeeLimit: test.feeLimit, FeeLimit: test.feeLimit,
ProbabilitySource: noProbabilitySource,
}, },
sourceNode.PubKeyBytes, target, paymentAmt, sourceNode.PubKeyBytes, target, paymentAmt,
) )
@ -934,9 +950,7 @@ func TestPathFindingWithAdditionalEdges(t *testing.T) {
graph: graph.graph, graph: graph.graph,
additionalEdges: additionalEdges, additionalEdges: additionalEdges,
}, },
&RestrictParams{ noRestrictions,
FeeLimit: noFeeLimit,
},
sourceNode.PubKeyBytes, doge.PubKeyBytes, paymentAmt, sourceNode.PubKeyBytes, doge.PubKeyBytes, paymentAmt,
) )
if err != nil { if err != nil {
@ -1190,9 +1204,7 @@ func TestNewRoutePathTooLong(t *testing.T) {
&graphParams{ &graphParams{
graph: graph.graph, graph: graph.graph,
}, },
&RestrictParams{ noRestrictions,
FeeLimit: noFeeLimit,
},
sourceNode.PubKeyBytes, target, paymentAmt, sourceNode.PubKeyBytes, target, paymentAmt,
) )
if err != nil { if err != nil {
@ -1206,9 +1218,7 @@ func TestNewRoutePathTooLong(t *testing.T) {
&graphParams{ &graphParams{
graph: graph.graph, graph: graph.graph,
}, },
&RestrictParams{ noRestrictions,
FeeLimit: noFeeLimit,
},
sourceNode.PubKeyBytes, target, paymentAmt, sourceNode.PubKeyBytes, target, paymentAmt,
) )
if err == nil { if err == nil {
@ -1248,9 +1258,7 @@ func TestPathNotAvailable(t *testing.T) {
&graphParams{ &graphParams{
graph: graph.graph, graph: graph.graph,
}, },
&RestrictParams{ noRestrictions,
FeeLimit: noFeeLimit,
},
sourceNode.PubKeyBytes, unknownNode, 100, sourceNode.PubKeyBytes, unknownNode, 100,
) )
if !IsError(err, ErrNoPathFound) { if !IsError(err, ErrNoPathFound) {
@ -1287,9 +1295,7 @@ func TestPathInsufficientCapacity(t *testing.T) {
&graphParams{ &graphParams{
graph: graph.graph, graph: graph.graph,
}, },
&RestrictParams{ noRestrictions,
FeeLimit: noFeeLimit,
},
sourceNode.PubKeyBytes, target, payAmt, sourceNode.PubKeyBytes, target, payAmt,
) )
if !IsError(err, ErrNoPathFound) { if !IsError(err, ErrNoPathFound) {
@ -1322,9 +1328,7 @@ func TestRouteFailMinHTLC(t *testing.T) {
&graphParams{ &graphParams{
graph: graph.graph, graph: graph.graph,
}, },
&RestrictParams{ noRestrictions,
FeeLimit: noFeeLimit,
},
sourceNode.PubKeyBytes, target, payAmt, sourceNode.PubKeyBytes, target, payAmt,
) )
if !IsError(err, ErrNoPathFound) { if !IsError(err, ErrNoPathFound) {
@ -1382,9 +1386,7 @@ func TestRouteFailMaxHTLC(t *testing.T) {
&graphParams{ &graphParams{
graph: graph.graph, graph: graph.graph,
}, },
&RestrictParams{ noRestrictions,
FeeLimit: noFeeLimit,
},
sourceNode.PubKeyBytes, target, payAmt, sourceNode.PubKeyBytes, target, payAmt,
) )
if err != nil { if err != nil {
@ -1406,9 +1408,7 @@ func TestRouteFailMaxHTLC(t *testing.T) {
&graphParams{ &graphParams{
graph: graph.graph, graph: graph.graph,
}, },
&RestrictParams{ noRestrictions,
FeeLimit: noFeeLimit,
},
sourceNode.PubKeyBytes, target, payAmt, sourceNode.PubKeyBytes, target, payAmt,
) )
if !IsError(err, ErrNoPathFound) { if !IsError(err, ErrNoPathFound) {
@ -1443,9 +1443,7 @@ func TestRouteFailDisabledEdge(t *testing.T) {
&graphParams{ &graphParams{
graph: graph.graph, graph: graph.graph,
}, },
&RestrictParams{ noRestrictions,
FeeLimit: noFeeLimit,
},
sourceNode.PubKeyBytes, target, payAmt, sourceNode.PubKeyBytes, target, payAmt,
) )
if err != nil { if err != nil {
@ -1473,9 +1471,7 @@ func TestRouteFailDisabledEdge(t *testing.T) {
&graphParams{ &graphParams{
graph: graph.graph, graph: graph.graph,
}, },
&RestrictParams{ noRestrictions,
FeeLimit: noFeeLimit,
},
sourceNode.PubKeyBytes, target, payAmt, sourceNode.PubKeyBytes, target, payAmt,
) )
if err != nil { if err != nil {
@ -1500,9 +1496,7 @@ func TestRouteFailDisabledEdge(t *testing.T) {
&graphParams{ &graphParams{
graph: graph.graph, graph: graph.graph,
}, },
&RestrictParams{ noRestrictions,
FeeLimit: noFeeLimit,
},
sourceNode.PubKeyBytes, target, payAmt, sourceNode.PubKeyBytes, target, payAmt,
) )
if !IsError(err, ErrNoPathFound) { if !IsError(err, ErrNoPathFound) {
@ -1536,9 +1530,7 @@ func TestPathSourceEdgesBandwidth(t *testing.T) {
&graphParams{ &graphParams{
graph: graph.graph, graph: graph.graph,
}, },
&RestrictParams{ noRestrictions,
FeeLimit: noFeeLimit,
},
sourceNode.PubKeyBytes, target, payAmt, sourceNode.PubKeyBytes, target, payAmt,
) )
if err != nil { if err != nil {
@ -1562,9 +1554,7 @@ func TestPathSourceEdgesBandwidth(t *testing.T) {
graph: graph.graph, graph: graph.graph,
bandwidthHints: bandwidths, bandwidthHints: bandwidths,
}, },
&RestrictParams{ noRestrictions,
FeeLimit: noFeeLimit,
},
sourceNode.PubKeyBytes, target, payAmt, sourceNode.PubKeyBytes, target, payAmt,
) )
if !IsError(err, ErrNoPathFound) { if !IsError(err, ErrNoPathFound) {
@ -1582,9 +1572,7 @@ func TestPathSourceEdgesBandwidth(t *testing.T) {
graph: graph.graph, graph: graph.graph,
bandwidthHints: bandwidths, bandwidthHints: bandwidths,
}, },
&RestrictParams{ noRestrictions,
FeeLimit: noFeeLimit,
},
sourceNode.PubKeyBytes, target, payAmt, sourceNode.PubKeyBytes, target, payAmt,
) )
if err != nil { if err != nil {
@ -1615,9 +1603,7 @@ func TestPathSourceEdgesBandwidth(t *testing.T) {
graph: graph.graph, graph: graph.graph,
bandwidthHints: bandwidths, bandwidthHints: bandwidths,
}, },
&RestrictParams{ noRestrictions,
FeeLimit: noFeeLimit,
},
sourceNode.PubKeyBytes, target, payAmt, sourceNode.PubKeyBytes, target, payAmt,
) )
if err != nil { if err != nil {
@ -1906,6 +1892,7 @@ func TestRestrictOutgoingChannel(t *testing.T) {
&RestrictParams{ &RestrictParams{
FeeLimit: noFeeLimit, FeeLimit: noFeeLimit,
OutgoingChannelID: &outgoingChannelID, OutgoingChannelID: &outgoingChannelID,
ProbabilitySource: noProbabilitySource,
}, },
sourceVertex, target, paymentAmt, sourceVertex, target, paymentAmt,
) )
@ -1982,9 +1969,6 @@ func testCltvLimit(t *testing.T, limit uint32, expectedChannel uint64) {
} }
sourceVertex := route.Vertex(sourceNode.PubKeyBytes) sourceVertex := route.Vertex(sourceNode.PubKeyBytes)
ignoredEdges := make(map[EdgeLocator]struct{})
ignoredVertexes := make(map[route.Vertex]struct{})
paymentAmt := lnwire.NewMSatFromSatoshis(100) paymentAmt := lnwire.NewMSatFromSatoshis(100)
target := testGraphInstance.aliasMap["target"] target := testGraphInstance.aliasMap["target"]
@ -1999,10 +1983,9 @@ func testCltvLimit(t *testing.T, limit uint32, expectedChannel uint64) {
graph: testGraphInstance.graph, graph: testGraphInstance.graph,
}, },
&RestrictParams{ &RestrictParams{
IgnoredNodes: ignoredVertexes,
IgnoredEdges: ignoredEdges,
FeeLimit: noFeeLimit, FeeLimit: noFeeLimit,
CltvLimit: cltvLimit, CltvLimit: cltvLimit,
ProbabilitySource: noProbabilitySource,
}, },
sourceVertex, target, paymentAmt, sourceVertex, target, paymentAmt,
) )
@ -2035,3 +2018,170 @@ func testCltvLimit(t *testing.T, limit uint32, expectedChannel uint64) {
route.Hops[0].ChannelID) route.Hops[0].ChannelID)
} }
} }
// TestProbabilityRouting asserts that path finding not only takes into account
// fees but also success probability.
func TestProbabilityRouting(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
p10, p11, p20 float64
minProbability float64
expectedChan uint64
}{
// Test two variations with probabilities that should multiply
// to the same total route probability. In both cases the three
// hop route should be the best route. The three hop route has a
// probability of 0.5 * 0.8 = 0.4. The fee is 5 (chan 10) + 8
// (chan 11) = 13. Path finding distance should work out to: 13
// + 10 (attempt penalty) / 0.4 = 38. The two hop route is 25 +
// 10 / 0.7 = 39.
{
name: "three hop 1",
p10: 0.8, p11: 0.5, p20: 0.7,
minProbability: 0.1,
expectedChan: 10,
},
{
name: "three hop 2",
p10: 0.5, p11: 0.8, p20: 0.7,
minProbability: 0.1,
expectedChan: 10,
},
// If the probability of the two hop route is increased, its
// distance becomes 25 + 10 / 0.85 = 37. This is less than the
// three hop route with its distance 38. So with an attempt
// penalty of 10, the higher fee route is chosen because of the
// compensation for success probability.
{
name: "two hop higher cost",
p10: 0.5, p11: 0.8, p20: 0.85,
minProbability: 0.1,
expectedChan: 20,
},
// If the same probabilities are used with a probability lower bound of
// 0.5, we expect the three hop route with probability 0.4 to be
// excluded and the two hop route to be picked.
{
name: "probability limit",
p10: 0.8, p11: 0.5, p20: 0.7,
minProbability: 0.5,
expectedChan: 20,
},
// With a probability limit above the probability of both routes, we
// expect no route to be returned. This expectation is signaled by using
// expected channel 0.
{
name: "probability limit no routes",
p10: 0.8, p11: 0.5, p20: 0.7,
minProbability: 0.8,
expectedChan: 0,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
testProbabilityRouting(
t, tc.p10, tc.p11, tc.p20,
tc.minProbability, tc.expectedChan,
)
})
}
}
func testProbabilityRouting(t *testing.T, p10, p11, p20, minProbability float64,
expectedChan uint64) {
t.Parallel()
// Set up a test graph with two possible paths to the target: a three
// hop path (via channels 10 and 11) and a two hop path (via channel
// 20).
testChannels := []*testChannel{
symmetricTestChannel("roasbeef", "a1", 100000, &testChannelPolicy{}),
symmetricTestChannel("roasbeef", "b", 100000, &testChannelPolicy{}),
symmetricTestChannel("a1", "a2", 100000, &testChannelPolicy{
Expiry: 144,
FeeBaseMsat: lnwire.NewMSatFromSatoshis(5),
MinHTLC: 1,
}, 10),
symmetricTestChannel("a2", "target", 100000, &testChannelPolicy{
Expiry: 144,
FeeBaseMsat: lnwire.NewMSatFromSatoshis(8),
MinHTLC: 1,
}, 11),
symmetricTestChannel("b", "target", 100000, &testChannelPolicy{
Expiry: 100,
FeeBaseMsat: lnwire.NewMSatFromSatoshis(25),
MinHTLC: 1,
}, 20),
}
testGraphInstance, err := createTestGraphFromChannels(testChannels)
if err != nil {
t.Fatalf("unable to create graph: %v", err)
}
defer testGraphInstance.cleanUp()
sourceNode, err := testGraphInstance.graph.SourceNode()
if err != nil {
t.Fatalf("unable to fetch source node: %v", err)
}
sourceVertex := route.Vertex(sourceNode.PubKeyBytes)
paymentAmt := lnwire.NewMSatFromSatoshis(100)
target := testGraphInstance.aliasMap["target"]
// Configure a probability source with the test parameters.
probabilitySource := func(node route.Vertex, edge EdgeLocator,
amt lnwire.MilliSatoshi) float64 {
if amt == 0 {
t.Fatal("expected non-zero amount")
}
switch edge.ChannelID {
case 10:
return p10
case 11:
return p11
case 20:
return p20
default:
return 1
}
}
path, err := findPath(
&graphParams{
graph: testGraphInstance.graph,
},
&RestrictParams{
FeeLimit: noFeeLimit,
ProbabilitySource: probabilitySource,
PaymentAttemptPenalty: lnwire.NewMSatFromSatoshis(10),
MinProbability: minProbability,
},
sourceVertex, target, paymentAmt,
)
if expectedChan == 0 {
if err == nil || !IsError(err, ErrNoPathFound) {
t.Fatalf("expected no path found, but got %v", err)
}
return
}
if err != nil {
t.Fatal(err)
}
// Assert that the route passes through the expected channel.
if path[1].ChannelID != expectedChan {
t.Fatalf("expected route to pass through channel %v, "+
"but channel %v was selected instead", expectedChan,
path[1].ChannelID)
}
}

@ -2,7 +2,6 @@ package routing
import ( import (
"fmt" "fmt"
"time"
"github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
@ -24,11 +23,12 @@ type PaymentSession interface {
// route. // route.
ReportVertexFailure(v route.Vertex) ReportVertexFailure(v route.Vertex)
// ReportEdgeFailure reports to the PaymentSession that the passed // ReportEdgeFailure reports to the PaymentSession that the passed edge
// channel failed to route the previous payment attempt. The // failed to route the previous payment attempt. A minimum penalization
// PaymentSession will use this information to produce a better next // amount is included to attenuate the failure. This is set to a
// route. // non-zero value for channel balance failures. The PaymentSession will
ReportEdgeFailure(e *EdgeLocator) // use this information to produce a better next route.
ReportEdgeFailure(failedEdge edge, minPenalizeAmt lnwire.MilliSatoshi)
// ReportEdgePolicyFailure reports to the PaymentSession that we // ReportEdgePolicyFailure reports to the PaymentSession that we
// received a failure message that relates to a channel policy. For // received a failure message that relates to a channel policy. For
@ -36,7 +36,7 @@ type PaymentSession interface {
// keep the edge included in the next attempted route. The // keep the edge included in the next attempted route. The
// PaymentSession will use this information to produce a better next // PaymentSession will use this information to produce a better next
// route. // route.
ReportEdgePolicyFailure(errSource route.Vertex, failedEdge *EdgeLocator) ReportEdgePolicyFailure(failedEdge edge)
} }
// paymentSession is used during an HTLC routings session to prune the local // paymentSession is used during an HTLC routings session to prune the local
@ -48,8 +48,6 @@ type PaymentSession interface {
// loop if payment attempts take long enough. An additional set of edges can // loop if payment attempts take long enough. An additional set of edges can
// also be provided to assist in reaching the payment's destination. // also be provided to assist in reaching the payment's destination.
type paymentSession struct { type paymentSession struct {
pruneViewSnapshot graphPruneView
additionalEdges map[route.Vertex][]*channeldb.ChannelEdgePolicy additionalEdges map[route.Vertex][]*channeldb.ChannelEdgePolicy
bandwidthHints map[uint64]lnwire.MilliSatoshi bandwidthHints map[uint64]lnwire.MilliSatoshi
@ -58,7 +56,7 @@ type paymentSession struct {
// source of policy related routing failures during this payment attempt. // source of policy related routing failures during this payment attempt.
// We'll use this map to prune out channels when the first error may not // We'll use this map to prune out channels when the first error may not
// require pruning, but any subsequent ones do. // require pruning, but any subsequent ones do.
errFailedPolicyChans map[EdgeLocator]struct{} errFailedPolicyChans map[nodeChannel]struct{}
mc *MissionControl mc *MissionControl
@ -80,17 +78,7 @@ var _ PaymentSession = (*paymentSession)(nil)
// //
// NOTE: Part of the PaymentSession interface. // NOTE: Part of the PaymentSession interface.
func (p *paymentSession) ReportVertexFailure(v route.Vertex) { func (p *paymentSession) ReportVertexFailure(v route.Vertex) {
log.Debugf("Reporting vertex %v failure to Mission Control", v) p.mc.reportVertexFailure(v)
// First, we'll add the failed vertex to our local prune view snapshot.
p.pruneViewSnapshot.vertexes[v] = struct{}{}
// With the vertex added, we'll now report back to the global prune
// view, with this new piece of information so it can be utilized for
// new payment sessions.
p.mc.Lock()
p.mc.failedVertexes[v] = time.Now()
p.mc.Unlock()
} }
// ReportEdgeFailure adds a channel to the graph prune view. The time the // ReportEdgeFailure adds a channel to the graph prune view. The time the
@ -102,18 +90,10 @@ func (p *paymentSession) ReportVertexFailure(v route.Vertex) {
// TODO(roasbeef): also add value attempted to send and capacity of channel // TODO(roasbeef): also add value attempted to send and capacity of channel
// //
// NOTE: Part of the PaymentSession interface. // NOTE: Part of the PaymentSession interface.
func (p *paymentSession) ReportEdgeFailure(e *EdgeLocator) { func (p *paymentSession) ReportEdgeFailure(failedEdge edge,
log.Debugf("Reporting edge %v failure to Mission Control", e) minPenalizeAmt lnwire.MilliSatoshi) {
// First, we'll add the failed edge to our local prune view snapshot. p.mc.reportEdgeFailure(failedEdge, minPenalizeAmt)
p.pruneViewSnapshot.edges[*e] = struct{}{}
// With the edge added, we'll now report back to the global prune view,
// with this new piece of information so it can be utilized for new
// payment sessions.
p.mc.Lock()
p.mc.failedEdges[*e] = time.Now()
p.mc.Unlock()
} }
// ReportEdgePolicyFailure handles a failure message that relates to a // ReportEdgePolicyFailure handles a failure message that relates to a
@ -124,23 +104,28 @@ func (p *paymentSession) ReportEdgeFailure(e *EdgeLocator) {
// new channel updates. // new channel updates.
// //
// NOTE: Part of the PaymentSession interface. // NOTE: Part of the PaymentSession interface.
func (p *paymentSession) ReportEdgePolicyFailure( //
errSource route.Vertex, failedEdge *EdgeLocator) { // TODO(joostjager): Move this logic into global mission control.
func (p *paymentSession) ReportEdgePolicyFailure(failedEdge edge) {
key := nodeChannel{
node: failedEdge.from,
channel: failedEdge.channel,
}
// Check to see if we've already reported a policy related failure for // Check to see if we've already reported a policy related failure for
// this channel. If so, then we'll prune out the vertex. // this channel. If so, then we'll prune out the vertex.
_, ok := p.errFailedPolicyChans[*failedEdge] _, ok := p.errFailedPolicyChans[key]
if ok { if ok {
// TODO(joostjager): is this aggressive pruning still necessary? // TODO(joostjager): is this aggressive pruning still necessary?
// Just pruning edges may also work unless there is a huge // Just pruning edges may also work unless there is a huge
// number of failing channels from that node? // number of failing channels from that node?
p.ReportVertexFailure(errSource) p.ReportVertexFailure(key.node)
return return
} }
// Finally, we'll record a policy failure from this node and move on. // Finally, we'll record a policy failure from this node and move on.
p.errFailedPolicyChans[*failedEdge] = struct{}{} p.errFailedPolicyChans[key] = struct{}{}
} }
// RequestRoute returns a route which is likely to be capable for successfully // RequestRoute returns a route which is likely to be capable for successfully
@ -169,15 +154,6 @@ func (p *paymentSession) RequestRoute(payment *LightningPayment,
return nil, fmt.Errorf("pre-built route already tried") return nil, fmt.Errorf("pre-built route already tried")
} }
// Otherwise we actually need to perform path finding, so we'll obtain
// our current prune view snapshot. This view will only ever grow
// during the duration of this payment session, never shrinking.
pruneView := p.pruneViewSnapshot
log.Debugf("Mission Control session using prune view of %v "+
"edges, %v vertexes", len(pruneView.edges),
len(pruneView.vertexes))
// If a route cltv limit was specified, we need to subtract the final // If a route cltv limit was specified, we need to subtract the final
// delta before passing it into path finding. The optimal path is // delta before passing it into path finding. The optimal path is
// independent of the final cltv delta and the path finding algorithm is // independent of the final cltv delta and the path finding algorithm is
@ -200,11 +176,12 @@ func (p *paymentSession) RequestRoute(payment *LightningPayment,
bandwidthHints: p.bandwidthHints, bandwidthHints: p.bandwidthHints,
}, },
&RestrictParams{ &RestrictParams{
IgnoredNodes: pruneView.vertexes, ProbabilitySource: p.mc.getEdgeProbability,
IgnoredEdges: pruneView.edges,
FeeLimit: payment.FeeLimit, FeeLimit: payment.FeeLimit,
OutgoingChannelID: payment.OutgoingChannelID, OutgoingChannelID: payment.OutgoingChannelID,
CltvLimit: cltvLimit, CltvLimit: cltvLimit,
PaymentAttemptPenalty: p.mc.cfg.PaymentAttemptPenalty,
MinProbability: p.mc.cfg.MinRouteProbability,
}, },
p.mc.selfNode.PubKeyBytes, payment.Target, p.mc.selfNode.PubKeyBytes, payment.Target,
payment.Amount, payment.Amount,
@ -227,3 +204,9 @@ func (p *paymentSession) RequestRoute(payment *LightningPayment,
return route, err return route, err
} }
// nodeChannel is a combination of the node pubkey and one of its channels.
type nodeChannel struct {
node route.Vertex
channel uint64
}

@ -35,8 +35,8 @@ func TestRequestRoute(t *testing.T) {
session := &paymentSession{ session := &paymentSession{
mc: &MissionControl{ mc: &MissionControl{
selfNode: &channeldb.LightningNode{}, selfNode: &channeldb.LightningNode{},
cfg: &MissionControlConfig{},
}, },
pruneViewSnapshot: graphPruneView{},
pathFinder: findPath, pathFinder: findPath,
} }

@ -3,6 +3,8 @@ package route
import ( import (
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"strconv"
"strings"
"github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/btcec"
sphinx "github.com/lightningnetwork/lightning-onion" sphinx "github.com/lightningnetwork/lightning-onion"
@ -180,3 +182,20 @@ func (r *Route) ToSphinxPath() (*sphinx.PaymentPath, error) {
return &path, nil return &path, nil
} }
// String returns a human readable representation of the route.
func (r *Route) String() string {
var b strings.Builder
for i, hop := range r.Hops {
if i > 0 {
b.WriteString(",")
}
b.WriteString(strconv.FormatUint(hop.ChannelID, 10))
}
return fmt.Sprintf("amt=%v, fees=%v, tl=%v, chans=%v",
r.TotalAmount-r.TotalFees(), r.TotalFees(), r.TotalTimeLock,
b.String(),
)
}

@ -319,6 +319,13 @@ func (e *EdgeLocator) String() string {
return fmt.Sprintf("%v:%v", e.ChannelID, e.Direction) return fmt.Sprintf("%v:%v", e.ChannelID, e.Direction)
} }
// edge is a combination of a channel and the node pubkeys of both of its
// endpoints.
type edge struct {
from, to route.Vertex
channel uint64
}
// ChannelRouter is the layer 3 router within the Lightning stack. Below the // ChannelRouter is the layer 3 router within the Lightning stack. Below the
// ChannelRouter is the HtlcSwitch, and below that is the Bitcoin blockchain // ChannelRouter is the HtlcSwitch, and below that is the Bitcoin blockchain
// itself. The primary role of the ChannelRouter is to respond to queries for // itself. The primary role of the ChannelRouter is to respond to queries for
@ -1598,7 +1605,9 @@ type LightningPayment struct {
// will be returned which describes the path the successful payment traversed // will be returned which describes the path the successful payment traversed
// within the network to reach the destination. Additionally, the payment // within the network to reach the destination. Additionally, the payment
// preimage will also be returned. // preimage will also be returned.
func (r *ChannelRouter) SendPayment(payment *LightningPayment) ([32]byte, *route.Route, error) { func (r *ChannelRouter) SendPayment(payment *LightningPayment) ([32]byte,
*route.Route, error) {
// Before starting the HTLC routing attempt, we'll create a fresh // Before starting the HTLC routing attempt, we'll create a fresh
// payment session which will report our errors back to mission // payment session which will report our errors back to mission
// control. // control.
@ -1772,7 +1781,9 @@ func (r *ChannelRouter) processSendError(paySession PaymentSession,
// Always determine chan id ourselves, because a channel // Always determine chan id ourselves, because a channel
// update with id may not be available. // update with id may not be available.
failedEdge, err := getFailedEdge(rt, route.Vertex(errVertex)) failedEdge, failedAmt, err := getFailedEdge(
rt, route.Vertex(errVertex),
)
if err != nil { if err != nil {
return true return true
} }
@ -1804,13 +1815,11 @@ func (r *ChannelRouter) processSendError(paySession PaymentSession,
// update to fail? // update to fail?
if !updateOk { if !updateOk {
paySession.ReportEdgeFailure( paySession.ReportEdgeFailure(
failedEdge, failedEdge, 0,
) )
} }
paySession.ReportEdgePolicyFailure( paySession.ReportEdgePolicyFailure(failedEdge)
route.NewVertex(errSource), failedEdge,
)
} }
switch onionErr := fErr.FailureMessage.(type) { switch onionErr := fErr.FailureMessage.(type) {
@ -1901,7 +1910,7 @@ func (r *ChannelRouter) processSendError(paySession PaymentSession,
// the update and continue. // the update and continue.
case *lnwire.FailChannelDisabled: case *lnwire.FailChannelDisabled:
r.applyChannelUpdate(&onionErr.Update, errSource) r.applyChannelUpdate(&onionErr.Update, errSource)
paySession.ReportEdgeFailure(failedEdge) paySession.ReportEdgeFailure(failedEdge, 0)
return false return false
// It's likely that the outgoing channel didn't have // It's likely that the outgoing channel didn't have
@ -1909,7 +1918,7 @@ func (r *ChannelRouter) processSendError(paySession PaymentSession,
// now, and continue onwards with our path finding. // now, and continue onwards with our path finding.
case *lnwire.FailTemporaryChannelFailure: case *lnwire.FailTemporaryChannelFailure:
r.applyChannelUpdate(onionErr.Update, errSource) r.applyChannelUpdate(onionErr.Update, errSource)
paySession.ReportEdgeFailure(failedEdge) paySession.ReportEdgeFailure(failedEdge, failedAmt)
return false return false
// If the send fail due to a node not having the // If the send fail due to a node not having the
@ -1934,7 +1943,7 @@ func (r *ChannelRouter) processSendError(paySession PaymentSession,
// returning errors in order to attempt to black list // returning errors in order to attempt to black list
// another node. // another node.
case *lnwire.FailUnknownNextPeer: case *lnwire.FailUnknownNextPeer:
paySession.ReportEdgeFailure(failedEdge) paySession.ReportEdgeFailure(failedEdge, 0)
return false return false
// If the node wasn't able to forward for which ever // If the node wasn't able to forward for which ever
@ -1965,14 +1974,12 @@ func (r *ChannelRouter) processSendError(paySession PaymentSession,
// we'll prune the channel in both directions and // we'll prune the channel in both directions and
// continue with the rest of the routes. // continue with the rest of the routes.
case *lnwire.FailPermanentChannelFailure: case *lnwire.FailPermanentChannelFailure:
paySession.ReportEdgeFailure(&EdgeLocator{ paySession.ReportEdgeFailure(failedEdge, 0)
ChannelID: failedEdge.ChannelID, paySession.ReportEdgeFailure(edge{
Direction: 0, from: failedEdge.to,
}) to: failedEdge.from,
paySession.ReportEdgeFailure(&EdgeLocator{ channel: failedEdge.channel,
ChannelID: failedEdge.ChannelID, }, 0)
Direction: 1,
})
return false return false
default: default:
@ -1982,12 +1989,14 @@ func (r *ChannelRouter) processSendError(paySession PaymentSession,
// getFailedEdge tries to locate the failing channel given a route and the // getFailedEdge tries to locate the failing channel given a route and the
// pubkey of the node that sent the error. It will assume that the error is // pubkey of the node that sent the error. It will assume that the error is
// associated with the outgoing channel of the error node. // associated with the outgoing channel of the error node. As a second result,
func getFailedEdge(route *route.Route, errSource route.Vertex) ( // it returns the amount sent over the edge.
*EdgeLocator, error) { func getFailedEdge(route *route.Route, errSource route.Vertex) (edge,
lnwire.MilliSatoshi, error) {
hopCount := len(route.Hops) hopCount := len(route.Hops)
fromNode := route.SourcePubKey fromNode := route.SourcePubKey
amt := route.TotalAmount
for i, hop := range route.Hops { for i, hop := range route.Hops {
toNode := hop.PubKeyBytes toNode := hop.PubKeyBytes
@ -2006,17 +2015,18 @@ func getFailedEdge(route *route.Route, errSource route.Vertex) (
// If the errSource is the final hop, we assume that the failing // If the errSource is the final hop, we assume that the failing
// channel is the incoming channel. // channel is the incoming channel.
if errSource == fromNode || finalHopFailing { if errSource == fromNode || finalHopFailing {
return newEdgeLocatorByPubkeys( return edge{
hop.ChannelID, from: fromNode,
&fromNode, to: toNode,
&toNode, channel: hop.ChannelID,
), nil }, amt, nil
} }
fromNode = toNode fromNode = toNode
amt = hop.AmtToForward
} }
return nil, fmt.Errorf("cannot find error source node in route") return edge{}, 0, fmt.Errorf("cannot find error source node in route")
} }
// applyChannelUpdate validates a channel update and if valid, applies it to the // applyChannelUpdate validates a channel update and if valid, applies it to the

@ -95,6 +95,12 @@ func createTestCtxFromGraphInstance(startingHeight uint32, graphInstance *testGr
func(e *channeldb.ChannelEdgeInfo) lnwire.MilliSatoshi { func(e *channeldb.ChannelEdgeInfo) lnwire.MilliSatoshi {
return lnwire.NewMSatFromSatoshis(e.Capacity) return lnwire.NewMSatFromSatoshis(e.Capacity)
}, },
&MissionControlConfig{
MinRouteProbability: 0.01,
PaymentAttemptPenalty: 100,
PenaltyHalfLife: time.Hour,
AprioriHopProbability: 0.9,
},
) )
router, err := New(Config{ router, err := New(Config{
Graph: graphInstance.graph, Graph: graphInstance.graph,
@ -202,6 +208,7 @@ func TestFindRoutesWithFeeLimit(t *testing.T) {
paymentAmt := lnwire.NewMSatFromSatoshis(100) paymentAmt := lnwire.NewMSatFromSatoshis(100)
restrictions := &RestrictParams{ restrictions := &RestrictParams{
FeeLimit: lnwire.NewMSatFromSatoshis(10), FeeLimit: lnwire.NewMSatFromSatoshis(10),
ProbabilitySource: noProbabilitySource,
} }
route, err := ctx.router.FindRoute( route, err := ctx.router.FindRoute(
@ -2198,9 +2205,7 @@ func TestFindPathFeeWeighting(t *testing.T) {
&graphParams{ &graphParams{
graph: ctx.graph, graph: ctx.graph,
}, },
&RestrictParams{ noRestrictions,
FeeLimit: noFeeLimit,
},
sourceNode.PubKeyBytes, target, amt, sourceNode.PubKeyBytes, target, amt,
) )
if err != nil { if err != nil {

@ -477,6 +477,7 @@ func newRPCServer(s *server, macService *macaroons.Service,
return info.NodeKey1Bytes, info.NodeKey2Bytes, nil return info.NodeKey1Bytes, info.NodeKey2Bytes, nil
}, },
FindRoute: s.chanRouter.FindRoute, FindRoute: s.chanRouter.FindRoute,
MissionControl: s.missionControl,
} }
var ( var (

@ -38,6 +38,7 @@ import (
"github.com/lightningnetwork/lnd/lncfg" "github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/lnpeer" "github.com/lightningnetwork/lnd/lnpeer"
"github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
"github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/nat" "github.com/lightningnetwork/lnd/nat"
@ -187,6 +188,8 @@ type server struct {
breachArbiter *breachArbiter breachArbiter *breachArbiter
missionControl *routing.MissionControl
chanRouter *routing.ChannelRouter chanRouter *routing.ChannelRouter
authGossiper *discovery.AuthenticatedGossiper authGossiper *discovery.AuthenticatedGossiper
@ -617,15 +620,6 @@ func newServer(listenAddrs []net.Addr, chanDB *channeldb.DB, cc *chainControl,
} }
queryBandwidth := func(edge *channeldb.ChannelEdgeInfo) lnwire.MilliSatoshi { queryBandwidth := func(edge *channeldb.ChannelEdgeInfo) lnwire.MilliSatoshi {
// If we aren't on either side of this edge, then we'll
// just thread through the capacity of the edge as we
// know it.
if !bytes.Equal(edge.NodeKey1Bytes[:], selfNode.PubKeyBytes[:]) &&
!bytes.Equal(edge.NodeKey2Bytes[:], selfNode.PubKeyBytes[:]) {
return lnwire.NewMSatFromSatoshis(edge.Capacity)
}
cid := lnwire.NewChanIDFromOutPoint(&edge.ChannelPoint) cid := lnwire.NewChanIDFromOutPoint(&edge.ChannelPoint)
link, err := s.htlcSwitch.GetLink(cid) link, err := s.htlcSwitch.GetLink(cid)
if err != nil { if err != nil {
@ -646,8 +640,13 @@ func newServer(listenAddrs []net.Addr, chanDB *channeldb.DB, cc *chainControl,
return link.Bandwidth() return link.Bandwidth()
} }
missionControl := routing.NewMissionControl( // Instantiate mission control with config from the sub server.
//
// TODO(joostjager): When we are further in the process of moving to sub
// servers, the mission control instance itself can be moved there too.
s.missionControl = routing.NewMissionControl(
chanGraph, selfNode, queryBandwidth, chanGraph, selfNode, queryBandwidth,
routerrpc.GetMissionControlConfig(cfg.SubRPCServers.RouterRPC),
) )
s.chanRouter, err = routing.New(routing.Config{ s.chanRouter, err = routing.New(routing.Config{
@ -656,7 +655,7 @@ func newServer(listenAddrs []net.Addr, chanDB *channeldb.DB, cc *chainControl,
ChainView: cc.chainView, ChainView: cc.chainView,
Payer: s.htlcSwitch, Payer: s.htlcSwitch,
Control: channeldb.NewPaymentControl(chanDB), Control: channeldb.NewPaymentControl(chanDB),
MissionControl: missionControl, MissionControl: s.missionControl,
ChannelPruneExpiry: routing.DefaultChannelPruneExpiry, ChannelPruneExpiry: routing.DefaultChannelPruneExpiry,
GraphPruneInterval: time.Duration(time.Hour), GraphPruneInterval: time.Duration(time.Hour),
QueryBandwidth: queryBandwidth, QueryBandwidth: queryBandwidth,