Merge pull request #2350 from halseth/autopilot-dryrun-only

[Autopilot] Add heuristic dry-run option
This commit is contained in:
Olaoluwa Osuntokun 2019-01-23 17:59:00 -08:00 committed by GitHub
commit ecd5541d55
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 524 additions and 39 deletions

@ -113,7 +113,8 @@ type Agent struct {
cfg Config
// chanState tracks the current set of open channels.
chanState channelState
chanState channelState
chanStateMtx sync.Mutex
// stateUpdates is a channel that any external state updates that may
// affect the heuristics of the agent will be sent over.
@ -410,7 +411,9 @@ func (a *Agent) controller() {
spew.Sdump(update.newChan))
newChan := update.newChan
a.chanStateMtx.Lock()
a.chanState[newChan.ChanID] = newChan
a.chanStateMtx.Unlock()
a.pendingMtx.Lock()
delete(a.pendingOpens, newChan.Node)
@ -424,9 +427,11 @@ func (a *Agent) controller() {
"updates: %v",
spew.Sdump(update.closedChans))
a.chanStateMtx.Lock()
for _, closedChan := range update.closedChans {
delete(a.chanState, closedChan)
}
a.chanStateMtx.Unlock()
updateBalance()
}
@ -472,10 +477,11 @@ func (a *Agent) controller() {
// With all the updates applied, we'll obtain a set of the
// current active channels (confirmed channels), and also
// factor in our set of unconfirmed channels.
confirmedChans := a.chanState
a.chanStateMtx.Lock()
a.pendingMtx.Lock()
totalChans := mergeChanState(a.pendingOpens, confirmedChans)
totalChans := mergeChanState(a.pendingOpens, a.chanState)
a.pendingMtx.Unlock()
a.chanStateMtx.Unlock()
// Now that we've updated our internal state, we'll consult our
// channel attachment heuristic to determine if we can open
@ -514,7 +520,10 @@ func (a *Agent) openChans(availableFunds btcutil.Amount, numChans uint32,
// We're to attempt an attachment so we'll obtain the set of
// nodes that we currently have channels with so we avoid
// duplicate edges.
a.chanStateMtx.Lock()
connectedNodes := a.chanState.ConnectedNodes()
a.chanStateMtx.Unlock()
a.pendingMtx.Lock()
nodesToSkip := mergeNodeMaps(a.pendingOpens,
a.pendingConns, connectedNodes, a.failedNodes,
@ -759,3 +768,55 @@ func (a *Agent) executeDirective(directive AttachmentDirective) {
// we'll trigger the autopilot agent to query for more peers.
a.OnChannelPendingOpen()
}
// HeuristicScores is an alias for a map that maps heuristic names to a map of
// scores for pubkeys.
type HeuristicScores map[string]map[NodeID]float64
// queryHeuristics gets node scores from all available simple heuristics, and
// the agent's current active heuristic.
func (a *Agent) queryHeuristics(nodes map[NodeID]struct{}) (
HeuristicScores, error) {
// Get the agent's current channel state.
a.chanStateMtx.Lock()
a.pendingMtx.Lock()
totalChans := mergeChanState(a.pendingOpens, a.chanState)
a.pendingMtx.Unlock()
a.chanStateMtx.Unlock()
// As channel size we'll use the maximum size.
chanSize := a.cfg.Constraints.MaxChanSize()
// We'll start by getting the scores from each available sub-heuristic,
// in addition the active agent heuristic.
report := make(HeuristicScores)
for _, h := range append(availableHeuristics, a.cfg.Heuristic) {
name := h.Name()
// If the active agent heuristic is among the simple heuristics
// it might get queried more than once. As an optimization
// we'll just skip it the second time.
if _, ok := report[name]; ok {
continue
}
s, err := h.NodeScores(
a.cfg.Graph, totalChans, chanSize, nodes,
)
if err != nil {
return nil, fmt.Errorf("unable to get sub score: %v", err)
}
log.Debugf("Heuristic \"%v\" scored %d nodes", name, len(s))
scores := make(map[NodeID]float64)
for nID, score := range s {
scores[nID] = score.Score
}
report[name] = scores
}
return report, nil
}

@ -80,6 +80,10 @@ type directiveArg struct {
nodes map[NodeID]struct{}
}
func (m *mockHeuristic) Name() string {
return "mock"
}
func (m *mockHeuristic) NodeScores(g ChannelGraph, chans []Channel,
fundsAvailable btcutil.Amount, nodes map[NodeID]struct{}) (
map[NodeID]*NodeScore, error) {

@ -47,6 +47,13 @@ func NewWeightedCombAttachment(h ...*WeightedHeuristic) (
// AttachmentHeuristic interface.
var _ AttachmentHeuristic = (*WeightedCombAttachment)(nil)
// Name returns the name of this heuristic.
//
// NOTE: This is a part of the AttachmentHeuristic interface.
func (c *WeightedCombAttachment) Name() string {
return "weightedcomb"
}
// NodeScores is a method that given the current channel graph, current set of
// local channels and funds available, scores the given nodes according to the
// preference of opening a channel with them. The returned channel candidates

@ -119,6 +119,9 @@ type AttachmentDirective struct {
// the interface is to allow an auto-pilot agent to decide if it needs more
// channels, and if so, which exact channels should be opened.
type AttachmentHeuristic interface {
// Name returns the name of this heuristic.
Name() string
// NodeScores is a method that given the current channel graph and
// current set of local channels, scores the given nodes according to
// the preference of opening a channel of the given size with them. The
@ -139,6 +142,27 @@ type AttachmentHeuristic interface {
map[NodeID]*NodeScore, error)
}
var (
// availableHeuristics holds all heuristics possible to combine for use
// with the autopilot agent.
availableHeuristics = []AttachmentHeuristic{
NewPrefAttachment(),
}
// AvailableHeuristics is a map that holds the name of available
// heuristics to the actual heuristic for easy lookup. It will be
// filled during init().
AvailableHeuristics = make(map[string]AttachmentHeuristic)
)
func init() {
// Fill the map from heuristic names to available heuristics for easy
// lookup.
for _, h := range availableHeuristics {
AvailableHeuristics[h.Name()] = h
}
}
// ChannelController is a simple interface that allows an auto-pilot agent to
// open a channel within the graph to a target peer, close targeted channels,
// or add/remove funds from existing channels via a splice in/out mechanisms.

@ -1,6 +1,7 @@
package autopilot
import (
"fmt"
"sync"
"sync/atomic"
@ -267,3 +268,22 @@ func (m *Manager) StopAgent() error {
return nil
}
// QueryHeuristics queries the active autopilot agent for node scores.
func (m *Manager) QueryHeuristics(nodes []NodeID) (HeuristicScores, error) {
m.Lock()
defer m.Unlock()
// Not active, so we can return early.
if m.pilot == nil {
return nil, fmt.Errorf("autopilot not active")
}
n := make(map[NodeID]struct{})
for _, node := range nodes {
n[node] = struct{}{}
}
log.Debugf("Querying heuristics for %d nodes", len(n))
return m.pilot.queryHeuristics(n)
}

@ -43,6 +43,13 @@ func NewNodeID(pub *btcec.PublicKey) NodeID {
return n
}
// Name returns the name of this heuristic.
//
// NOTE: This is a part of the AttachmentHeuristic interface.
func (p *PrefAttachment) Name() string {
return "preferential"
}
// NodeScores is a method that given the current channel graph and current set
// of local channels, scores the given nodes according to the preference of
// opening a channel of the given size with them. The returned channel

@ -94,6 +94,47 @@ func disable(ctx *cli.Context) error {
return nil
}
var queryScoresCommand = cli.Command{
Name: "query",
Usage: "Query the autopilot heuristcs for nodes' scores.",
ArgsUsage: "<pubkey> <pubkey> <pubkey> ...",
Description: "",
Action: actionDecorator(queryScores),
}
func queryScores(ctx *cli.Context) error {
ctxb := context.Background()
client, cleanUp := getAutopilotClient(ctx)
defer cleanUp()
args := ctx.Args()
var pubs []string
// Keep reading pubkeys as long as there are arguments.
loop:
for {
switch {
case args.Present():
pubs = append(pubs, args.First())
args = args.Tail()
default:
break loop
}
}
req := &autopilotrpc.QueryScoresRequest{
Pubkeys: pubs,
}
resp, err := client.QueryScores(ctxb, req)
if err != nil {
return err
}
printRespJSON(resp)
return nil
}
// autopilotCommands will return the set of commands to enable for autopilotrpc
// builds.
func autopilotCommands() []cli.Command {
@ -107,6 +148,7 @@ func autopilotCommands() []cli.Command {
getStatusCommand,
enableCommand,
disableCommand,
queryScoresCommand,
},
},
}

@ -142,13 +142,14 @@ type bitcoindConfig struct {
}
type autoPilotConfig struct {
Active bool `long:"active" description:"If the autopilot agent should be active or not."`
MaxChannels int `long:"maxchannels" description:"The maximum number of channels that should be created"`
Allocation float64 `long:"allocation" description:"The percentage of total funds that should be committed to automatic channel establishment"`
MinChannelSize int64 `long:"minchansize" description:"The smallest channel that the autopilot agent should create"`
MaxChannelSize int64 `long:"maxchansize" description:"The largest channel that the autopilot agent should create"`
Private bool `long:"private" description:"Whether the channels created by the autopilot agent should be private or not. Private channels won't be announced to the network."`
MinConfs int32 `long:"minconfs" description:"The minimum number of confirmations each of your inputs in funding transactions created by the autopilot agent must have."`
Active bool `long:"active" description:"If the autopilot agent should be active or not."`
Heuristic map[string]float64 `long:"heuristic" description:"Heuristic to activate, and the weight to give it during scoring."`
MaxChannels int `long:"maxchannels" description:"The maximum number of channels that should be created"`
Allocation float64 `long:"allocation" description:"The percentage of total funds that should be committed to automatic channel establishment"`
MinChannelSize int64 `long:"minchansize" description:"The smallest channel that the autopilot agent should create"`
MaxChannelSize int64 `long:"maxchansize" description:"The largest channel that the autopilot agent should create"`
Private bool `long:"private" description:"Whether the channels created by the autopilot agent should be private or not. Private channels won't be announced to the network."`
MinConfs int32 `long:"minconfs" description:"The minimum number of confirmations each of your inputs in funding transactions created by the autopilot agent must have."`
}
type torConfig struct {
@ -312,6 +313,9 @@ func loadConfig() (*config, error) {
Allocation: 0.6,
MinChannelSize: int64(minChanFundingSize),
MaxChannelSize: int64(maxFundingAmount),
Heuristic: map[string]float64{
"preferential": 1.0,
},
},
TrickleDelay: defaultTrickleDelay,
InactiveChanTimeout: defaultInactiveChanTimeout,
@ -463,6 +467,10 @@ func loadConfig() (*config, error) {
cfg.Autopilot.MaxChannelSize = int64(maxFundingAmount)
}
if _, err := validateAtplCfg(cfg.Autopilot); err != nil {
return nil, err
}
// Validate the Tor config parameters.
socks, err := lncfg.ParseAddressString(
cfg.Tor.SOCKS, strconv.Itoa(defaultTorSOCKSPort),

@ -33,7 +33,7 @@ func (m *StatusRequest) Reset() { *m = StatusRequest{} }
func (m *StatusRequest) String() string { return proto.CompactTextString(m) }
func (*StatusRequest) ProtoMessage() {}
func (*StatusRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_autopilot_45e6f1df6dc1d1df, []int{0}
return fileDescriptor_autopilot_52f30cf4d0055211, []int{0}
}
func (m *StatusRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_StatusRequest.Unmarshal(m, b)
@ -65,7 +65,7 @@ func (m *StatusResponse) Reset() { *m = StatusResponse{} }
func (m *StatusResponse) String() string { return proto.CompactTextString(m) }
func (*StatusResponse) ProtoMessage() {}
func (*StatusResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_autopilot_45e6f1df6dc1d1df, []int{1}
return fileDescriptor_autopilot_52f30cf4d0055211, []int{1}
}
func (m *StatusResponse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_StatusResponse.Unmarshal(m, b)
@ -104,7 +104,7 @@ func (m *ModifyStatusRequest) Reset() { *m = ModifyStatusRequest{} }
func (m *ModifyStatusRequest) String() string { return proto.CompactTextString(m) }
func (*ModifyStatusRequest) ProtoMessage() {}
func (*ModifyStatusRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_autopilot_45e6f1df6dc1d1df, []int{2}
return fileDescriptor_autopilot_52f30cf4d0055211, []int{2}
}
func (m *ModifyStatusRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_ModifyStatusRequest.Unmarshal(m, b)
@ -141,7 +141,7 @@ func (m *ModifyStatusResponse) Reset() { *m = ModifyStatusResponse{} }
func (m *ModifyStatusResponse) String() string { return proto.CompactTextString(m) }
func (*ModifyStatusResponse) ProtoMessage() {}
func (*ModifyStatusResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_autopilot_45e6f1df6dc1d1df, []int{3}
return fileDescriptor_autopilot_52f30cf4d0055211, []int{3}
}
func (m *ModifyStatusResponse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_ModifyStatusResponse.Unmarshal(m, b)
@ -161,11 +161,137 @@ func (m *ModifyStatusResponse) XXX_DiscardUnknown() {
var xxx_messageInfo_ModifyStatusResponse proto.InternalMessageInfo
type QueryScoresRequest struct {
Pubkeys []string `protobuf:"bytes,1,rep,name=pubkeys,proto3" json:"pubkeys,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *QueryScoresRequest) Reset() { *m = QueryScoresRequest{} }
func (m *QueryScoresRequest) String() string { return proto.CompactTextString(m) }
func (*QueryScoresRequest) ProtoMessage() {}
func (*QueryScoresRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_autopilot_52f30cf4d0055211, []int{4}
}
func (m *QueryScoresRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_QueryScoresRequest.Unmarshal(m, b)
}
func (m *QueryScoresRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_QueryScoresRequest.Marshal(b, m, deterministic)
}
func (dst *QueryScoresRequest) XXX_Merge(src proto.Message) {
xxx_messageInfo_QueryScoresRequest.Merge(dst, src)
}
func (m *QueryScoresRequest) XXX_Size() int {
return xxx_messageInfo_QueryScoresRequest.Size(m)
}
func (m *QueryScoresRequest) XXX_DiscardUnknown() {
xxx_messageInfo_QueryScoresRequest.DiscardUnknown(m)
}
var xxx_messageInfo_QueryScoresRequest proto.InternalMessageInfo
func (m *QueryScoresRequest) GetPubkeys() []string {
if m != nil {
return m.Pubkeys
}
return nil
}
type QueryScoresResponse struct {
Results []*QueryScoresResponse_HeuristicResult `protobuf:"bytes,1,rep,name=results,proto3" json:"results,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *QueryScoresResponse) Reset() { *m = QueryScoresResponse{} }
func (m *QueryScoresResponse) String() string { return proto.CompactTextString(m) }
func (*QueryScoresResponse) ProtoMessage() {}
func (*QueryScoresResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_autopilot_52f30cf4d0055211, []int{5}
}
func (m *QueryScoresResponse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_QueryScoresResponse.Unmarshal(m, b)
}
func (m *QueryScoresResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_QueryScoresResponse.Marshal(b, m, deterministic)
}
func (dst *QueryScoresResponse) XXX_Merge(src proto.Message) {
xxx_messageInfo_QueryScoresResponse.Merge(dst, src)
}
func (m *QueryScoresResponse) XXX_Size() int {
return xxx_messageInfo_QueryScoresResponse.Size(m)
}
func (m *QueryScoresResponse) XXX_DiscardUnknown() {
xxx_messageInfo_QueryScoresResponse.DiscardUnknown(m)
}
var xxx_messageInfo_QueryScoresResponse proto.InternalMessageInfo
func (m *QueryScoresResponse) GetResults() []*QueryScoresResponse_HeuristicResult {
if m != nil {
return m.Results
}
return nil
}
type QueryScoresResponse_HeuristicResult struct {
Heuristic string `protobuf:"bytes,1,opt,name=heuristic,proto3" json:"heuristic,omitempty"`
Scores map[string]float64 `protobuf:"bytes,2,rep,name=scores,proto3" json:"scores,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"fixed64,2,opt,name=value,proto3"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *QueryScoresResponse_HeuristicResult) Reset() { *m = QueryScoresResponse_HeuristicResult{} }
func (m *QueryScoresResponse_HeuristicResult) String() string { return proto.CompactTextString(m) }
func (*QueryScoresResponse_HeuristicResult) ProtoMessage() {}
func (*QueryScoresResponse_HeuristicResult) Descriptor() ([]byte, []int) {
return fileDescriptor_autopilot_52f30cf4d0055211, []int{5, 0}
}
func (m *QueryScoresResponse_HeuristicResult) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_QueryScoresResponse_HeuristicResult.Unmarshal(m, b)
}
func (m *QueryScoresResponse_HeuristicResult) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_QueryScoresResponse_HeuristicResult.Marshal(b, m, deterministic)
}
func (dst *QueryScoresResponse_HeuristicResult) XXX_Merge(src proto.Message) {
xxx_messageInfo_QueryScoresResponse_HeuristicResult.Merge(dst, src)
}
func (m *QueryScoresResponse_HeuristicResult) XXX_Size() int {
return xxx_messageInfo_QueryScoresResponse_HeuristicResult.Size(m)
}
func (m *QueryScoresResponse_HeuristicResult) XXX_DiscardUnknown() {
xxx_messageInfo_QueryScoresResponse_HeuristicResult.DiscardUnknown(m)
}
var xxx_messageInfo_QueryScoresResponse_HeuristicResult proto.InternalMessageInfo
func (m *QueryScoresResponse_HeuristicResult) GetHeuristic() string {
if m != nil {
return m.Heuristic
}
return ""
}
func (m *QueryScoresResponse_HeuristicResult) GetScores() map[string]float64 {
if m != nil {
return m.Scores
}
return nil
}
func init() {
proto.RegisterType((*StatusRequest)(nil), "autopilotrpc.StatusRequest")
proto.RegisterType((*StatusResponse)(nil), "autopilotrpc.StatusResponse")
proto.RegisterType((*ModifyStatusRequest)(nil), "autopilotrpc.ModifyStatusRequest")
proto.RegisterType((*ModifyStatusResponse)(nil), "autopilotrpc.ModifyStatusResponse")
proto.RegisterType((*QueryScoresRequest)(nil), "autopilotrpc.QueryScoresRequest")
proto.RegisterType((*QueryScoresResponse)(nil), "autopilotrpc.QueryScoresResponse")
proto.RegisterType((*QueryScoresResponse_HeuristicResult)(nil), "autopilotrpc.QueryScoresResponse.HeuristicResult")
proto.RegisterMapType((map[string]float64)(nil), "autopilotrpc.QueryScoresResponse.HeuristicResult.ScoresEntry")
}
// Reference imports to suppress errors if they are not otherwise used.
@ -187,6 +313,11 @@ type AutopilotClient interface {
// ModifyStatus is used to modify the status of the autopilot agent, like
// enabling or disabling it.
ModifyStatus(ctx context.Context, in *ModifyStatusRequest, opts ...grpc.CallOption) (*ModifyStatusResponse, error)
// *
// QueryScores queries all available autopilot heuristics, in addition to any
// active combination of these heruristics, for the scores they would give to
// the given nodes.
QueryScores(ctx context.Context, in *QueryScoresRequest, opts ...grpc.CallOption) (*QueryScoresResponse, error)
}
type autopilotClient struct {
@ -215,6 +346,15 @@ func (c *autopilotClient) ModifyStatus(ctx context.Context, in *ModifyStatusRequ
return out, nil
}
func (c *autopilotClient) QueryScores(ctx context.Context, in *QueryScoresRequest, opts ...grpc.CallOption) (*QueryScoresResponse, error) {
out := new(QueryScoresResponse)
err := c.cc.Invoke(ctx, "/autopilotrpc.Autopilot/QueryScores", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// AutopilotServer is the server API for Autopilot service.
type AutopilotServer interface {
// *
@ -224,6 +364,11 @@ type AutopilotServer interface {
// ModifyStatus is used to modify the status of the autopilot agent, like
// enabling or disabling it.
ModifyStatus(context.Context, *ModifyStatusRequest) (*ModifyStatusResponse, error)
// *
// QueryScores queries all available autopilot heuristics, in addition to any
// active combination of these heruristics, for the scores they would give to
// the given nodes.
QueryScores(context.Context, *QueryScoresRequest) (*QueryScoresResponse, error)
}
func RegisterAutopilotServer(s *grpc.Server, srv AutopilotServer) {
@ -266,6 +411,24 @@ func _Autopilot_ModifyStatus_Handler(srv interface{}, ctx context.Context, dec f
return interceptor(ctx, in, info, handler)
}
func _Autopilot_QueryScores_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(QueryScoresRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AutopilotServer).QueryScores(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/autopilotrpc.Autopilot/QueryScores",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AutopilotServer).QueryScores(ctx, req.(*QueryScoresRequest))
}
return interceptor(ctx, in, info, handler)
}
var _Autopilot_serviceDesc = grpc.ServiceDesc{
ServiceName: "autopilotrpc.Autopilot",
HandlerType: (*AutopilotServer)(nil),
@ -278,30 +441,44 @@ var _Autopilot_serviceDesc = grpc.ServiceDesc{
MethodName: "ModifyStatus",
Handler: _Autopilot_ModifyStatus_Handler,
},
{
MethodName: "QueryScores",
Handler: _Autopilot_QueryScores_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "autopilotrpc/autopilot.proto",
}
func init() {
proto.RegisterFile("autopilotrpc/autopilot.proto", fileDescriptor_autopilot_45e6f1df6dc1d1df)
proto.RegisterFile("autopilotrpc/autopilot.proto", fileDescriptor_autopilot_52f30cf4d0055211)
}
var fileDescriptor_autopilot_45e6f1df6dc1d1df = []byte{
// 226 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0x49, 0x2c, 0x2d, 0xc9,
0x2f, 0xc8, 0xcc, 0xc9, 0x2f, 0x29, 0x2a, 0x48, 0xd6, 0x87, 0x73, 0xf4, 0x0a, 0x8a, 0xf2, 0x4b,
0xf2, 0x85, 0x78, 0x90, 0x65, 0x95, 0xf8, 0xb9, 0x78, 0x83, 0x4b, 0x12, 0x4b, 0x4a, 0x8b, 0x83,
0x52, 0x0b, 0x4b, 0x53, 0x8b, 0x4b, 0x94, 0x34, 0xb8, 0xf8, 0x60, 0x02, 0xc5, 0x05, 0xf9, 0x79,
0xc5, 0xa9, 0x42, 0x62, 0x5c, 0x6c, 0x89, 0xc9, 0x25, 0x99, 0x65, 0xa9, 0x12, 0x8c, 0x0a, 0x8c,
0x1a, 0x1c, 0x41, 0x50, 0x9e, 0x92, 0x2e, 0x97, 0xb0, 0x6f, 0x7e, 0x4a, 0x66, 0x5a, 0x25, 0x8a,
0x01, 0x20, 0xe5, 0xa9, 0x79, 0x89, 0x49, 0x39, 0x70, 0xe5, 0x10, 0x9e, 0x92, 0x18, 0x97, 0x08,
0xaa, 0x72, 0x88, 0xf1, 0x46, 0xcb, 0x19, 0xb9, 0x38, 0x1d, 0x61, 0x4e, 0x12, 0x72, 0xe6, 0x62,
0x83, 0xc8, 0x0b, 0x49, 0xeb, 0x21, 0x3b, 0x54, 0x0f, 0xc5, 0x12, 0x29, 0x19, 0xec, 0x92, 0x50,
0x17, 0x87, 0x72, 0xf1, 0x20, 0x5b, 0x25, 0xa4, 0x88, 0xaa, 0x1a, 0x8b, 0xab, 0xa5, 0x94, 0xf0,
0x29, 0x81, 0x18, 0xeb, 0x64, 0x12, 0x65, 0x94, 0x9e, 0x59, 0x92, 0x51, 0x9a, 0xa4, 0x97, 0x9c,
0x9f, 0xab, 0x9f, 0x93, 0x99, 0x9e, 0x51, 0x92, 0x97, 0x99, 0x97, 0x9e, 0x97, 0x5a, 0x52, 0x9e,
0x5f, 0x94, 0xad, 0x9f, 0x93, 0x97, 0xa2, 0x9f, 0x93, 0x87, 0x12, 0xe4, 0x45, 0x05, 0xc9, 0x49,
0x6c, 0xe0, 0x60, 0x37, 0x06, 0x04, 0x00, 0x00, 0xff, 0xff, 0xd1, 0x71, 0xb3, 0xba, 0x96, 0x01,
0x00, 0x00,
var fileDescriptor_autopilot_52f30cf4d0055211 = []byte{
// 391 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x53, 0x4d, 0xaf, 0xd2, 0x40,
0x14, 0xcd, 0x94, 0x58, 0xec, 0x05, 0xc5, 0x0c, 0x84, 0x34, 0x95, 0x45, 0xe9, 0xaa, 0x1b, 0xdb,
0x88, 0x2e, 0xd4, 0xc4, 0x85, 0x1a, 0x13, 0x13, 0xe3, 0xc2, 0x21, 0x6c, 0xdc, 0xb5, 0x65, 0x84,
0x09, 0x75, 0xa6, 0xce, 0x07, 0xa6, 0x7f, 0xc8, 0xff, 0xe1, 0xef, 0x72, 0xf3, 0x42, 0x3f, 0x78,
0xed, 0x0b, 0xe1, 0xe5, 0xed, 0x7a, 0xee, 0x3d, 0xe7, 0xdc, 0x3b, 0x67, 0x3a, 0xb0, 0x48, 0x8c,
0x16, 0x05, 0xcb, 0x85, 0x96, 0x45, 0x16, 0x9f, 0x41, 0x54, 0x48, 0xa1, 0x05, 0x1e, 0x77, 0xbb,
0xc1, 0x04, 0x9e, 0xac, 0x75, 0xa2, 0x8d, 0x22, 0xf4, 0xb7, 0xa1, 0x4a, 0x07, 0x21, 0x3c, 0x6d,
0x0b, 0xaa, 0x10, 0x5c, 0x51, 0x3c, 0x07, 0x3b, 0xc9, 0x34, 0x3b, 0x52, 0x17, 0xf9, 0x28, 0x7c,
0x4c, 0x1a, 0x14, 0xbc, 0x80, 0xe9, 0x37, 0xb1, 0x65, 0x3f, 0xcb, 0x9e, 0xc1, 0x89, 0x4e, 0x79,
0x92, 0xe6, 0x67, 0x7a, 0x8d, 0x82, 0x39, 0xcc, 0xfa, 0xf4, 0xda, 0x3e, 0x88, 0x00, 0x7f, 0x37,
0x54, 0x96, 0xeb, 0x4c, 0x48, 0x7a, 0x76, 0x71, 0x61, 0x58, 0x98, 0xf4, 0x40, 0x4b, 0xe5, 0x22,
0x7f, 0x10, 0x3a, 0xa4, 0x85, 0xc1, 0x5f, 0x0b, 0xa6, 0x3d, 0x41, 0xb3, 0xe6, 0x57, 0x18, 0x4a,
0xaa, 0x4c, 0xae, 0x6b, 0xc5, 0x68, 0xf5, 0x32, 0xea, 0x9e, 0x34, 0xba, 0xa0, 0x89, 0xbe, 0x50,
0x23, 0x99, 0xd2, 0x2c, 0x23, 0x95, 0x92, 0xb4, 0x0e, 0xde, 0x3f, 0x04, 0x93, 0x3b, 0x4d, 0xbc,
0x00, 0x67, 0xdf, 0x96, 0xaa, 0xb3, 0x39, 0xe4, 0xb6, 0x80, 0x37, 0x60, 0xab, 0xca, 0xdc, 0xb5,
0xaa, 0xe9, 0xef, 0x1f, 0x3c, 0x3d, 0xaa, 0xdb, 0x9f, 0xb9, 0x96, 0x25, 0x69, 0xcc, 0xbc, 0xb7,
0x30, 0xea, 0x94, 0xf1, 0x33, 0x18, 0x1c, 0x68, 0xd9, 0x4c, 0x3f, 0x7d, 0xe2, 0x19, 0x3c, 0x3a,
0x26, 0xb9, 0xa1, 0xae, 0xe5, 0xa3, 0x10, 0x91, 0x1a, 0xbc, 0xb3, 0xde, 0xa0, 0xd5, 0x7f, 0x04,
0xce, 0x87, 0x76, 0x07, 0xfc, 0x09, 0xec, 0x3a, 0x78, 0xfc, 0xbc, 0xbf, 0x59, 0xef, 0xf6, 0xbc,
0xc5, 0xe5, 0x66, 0x93, 0xf1, 0x06, 0xc6, 0xdd, 0x3b, 0xc4, 0xcb, 0x3e, 0xfb, 0xc2, 0xef, 0xe0,
0x05, 0xd7, 0x28, 0x8d, 0x2d, 0x81, 0x51, 0x27, 0x1f, 0xec, 0x5f, 0x89, 0xae, 0x36, 0x5d, 0xde,
0x1b, 0xee, 0xc7, 0xd7, 0x3f, 0x56, 0x3b, 0xa6, 0xf7, 0x26, 0x8d, 0x32, 0xf1, 0x2b, 0xce, 0xd9,
0x6e, 0xaf, 0x39, 0xe3, 0x3b, 0x4e, 0xf5, 0x1f, 0x21, 0x0f, 0x71, 0xce, 0xb7, 0x71, 0xce, 0x7b,
0xef, 0x43, 0x16, 0x59, 0x6a, 0x57, 0x6f, 0xe4, 0xd5, 0x4d, 0x00, 0x00, 0x00, 0xff, 0xff, 0xa1,
0xe9, 0x30, 0xf3, 0x43, 0x03, 0x00, 0x00,
}

@ -18,6 +18,13 @@ service Autopilot {
enabling or disabling it.
*/
rpc ModifyStatus(ModifyStatusRequest) returns (ModifyStatusResponse);
/**
QueryScores queries all available autopilot heuristics, in addition to any
active combination of these heruristics, for the scores they would give to
the given nodes.
*/
rpc QueryScores(QueryScoresRequest) returns (QueryScoresResponse);
}
message StatusRequest{
@ -34,3 +41,16 @@ message ModifyStatusRequest{
}
message ModifyStatusResponse {}
message QueryScoresRequest{
repeated string pubkeys = 1 [json_name = "pubkeys"];
}
message QueryScoresResponse {
message HeuristicResult {
string heuristic = 1 [json_name = "heuristic"];
map<string, double> scores= 2 [json_name = "scores"];
}
repeated HeuristicResult results = 1 [json_name = "results"];
}

@ -4,9 +4,11 @@ package autopilotrpc
import (
"context"
"encoding/hex"
"os"
"sync/atomic"
"github.com/btcsuite/btcd/btcec"
"github.com/lightningnetwork/lnd/autopilot"
"github.com/lightningnetwork/lnd/lnrpc"
"google.golang.org/grpc"
@ -35,6 +37,10 @@ var (
Entity: "offchain",
Action: "write",
}},
"/autopilotrpc.Autopilot/QueryScores": {{
Entity: "info",
Action: "read",
}},
}
)
@ -154,3 +160,60 @@ func (s *Server) ModifyStatus(ctx context.Context,
}
return &ModifyStatusResponse{}, err
}
// QueryScores queries all available autopilot heuristics, in addition to any
// active combination of these heruristics, for the scores they would give to
// the given nodes.
//
// NOTE: Part of the AutopilotServer interface.
func (s *Server) QueryScores(ctx context.Context, in *QueryScoresRequest) (
*QueryScoresResponse, error) {
var nodes []autopilot.NodeID
for _, pubStr := range in.Pubkeys {
pubHex, err := hex.DecodeString(pubStr)
if err != nil {
return nil, err
}
pubKey, err := btcec.ParsePubKey(pubHex, btcec.S256())
if err != nil {
return nil, err
}
nID := autopilot.NewNodeID(pubKey)
nodes = append(nodes, nID)
}
// Query the heuristics.
heuristicScores, err := s.manager.QueryHeuristics(nodes)
if err != nil {
return nil, err
}
resp := &QueryScoresResponse{}
for heuristic, scores := range heuristicScores {
result := &QueryScoresResponse_HeuristicResult{
Heuristic: heuristic,
Scores: make(map[string]float64),
}
for pub, score := range scores {
pubkeyHex := hex.EncodeToString(pub[:])
result.Scores[pubkeyHex] = score
}
// Since a node not being part of the internally returned
// scores imply a zero score, we add these before we return the
// RPC results.
for _, node := range nodes {
if _, ok := scores[node]; ok {
continue
}
pubkeyHex := hex.EncodeToString(node[:])
result.Scores[pubkeyHex] = 0.0
}
resp.Results = append(resp.Results, result)
}
return resp, nil
}

@ -14,6 +14,60 @@ import (
"github.com/lightningnetwork/lnd/tor"
)
// validateAtplConfig is a helper method that makes sure the passed
// configuration is sane. Currently it checks that the heuristic configuration
// makes sense. In case the config is valid, it will return a list of
// WeightedHeuristics that can be combined for use with the autopilot agent.
func validateAtplCfg(cfg *autoPilotConfig) ([]*autopilot.WeightedHeuristic,
error) {
var (
heuristicsStr string
sum float64
heuristics []*autopilot.WeightedHeuristic
)
// Create a help text that we can return in case the config is not
// correct.
for _, a := range autopilot.AvailableHeuristics {
heuristicsStr += fmt.Sprintf(" '%v' ", a.Name())
}
availStr := fmt.Sprintf("Avaiblable heuristcs are: [%v]", heuristicsStr)
// We'll go through the config and make sure all the heuristics exists,
// and that the sum of their weights is 1.0.
for name, weight := range cfg.Heuristic {
a, ok := autopilot.AvailableHeuristics[name]
if !ok {
// No heuristic matching this config option was found.
return nil, fmt.Errorf("Heuristic %v not available. %v",
name, availStr)
}
// If this heuristic was among the registered ones, we add it
// to the list we'll give to the agent, and keep track of the
// sum of weights.
heuristics = append(
heuristics,
&autopilot.WeightedHeuristic{
Weight: weight,
AttachmentHeuristic: a,
},
)
sum += weight
}
// Check found heuristics. We must have at least one to operate.
if len(heuristics) == 0 {
return nil, fmt.Errorf("No active heuristics. %v", availStr)
}
if sum != 1.0 {
return nil, fmt.Errorf("Heuristic weights must sum to 1.0")
}
return heuristics, nil
}
// chanController is an implementation of the autopilot.ChannelController
// interface that's backed by a running lnd instance.
type chanController struct {
@ -94,15 +148,13 @@ func initAutoPilot(svr *server, cfg *autoPilotConfig) (*autopilot.ManagerCfg, er
10,
cfg.Allocation,
)
// First, we'll create the preferential attachment heuristic.
prefAttachment := autopilot.NewPrefAttachment()
heuristics, err := validateAtplCfg(cfg)
if err != nil {
return nil, err
}
weightedAttachment, err := autopilot.NewWeightedCombAttachment(
&autopilot.WeightedHeuristic{
Weight: 1.0,
AttachmentHeuristic: prefAttachment,
},
heuristics...,
)
if err != nil {
return nil, err