diff --git a/.travis.yml b/.travis.yml index bedd772..2aa6560 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,6 +24,7 @@ sudo: required script: - export GO111MODULE=on - make lint unit build mod-check + - make tags=dev after_script: - echo "Uploading to termbin.com..." && find *.log | xargs -I{} sh -c "cat {} | nc termbin.com 9999 | xargs -r0 printf '{} uploaded to %s'" diff --git a/Makefile b/Makefile index 3aea970..650a98b 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,7 @@ GOMOD := GO111MODULE=on go mod COMMIT := $(shell git describe --abbrev=40 --dirty) LDFLAGS := -ldflags "-X $(PKG)/build.Commit=$(COMMIT)" +DEV_TAGS = dev GOFILES_NOVENDOR = $(shell find . -type f -name '*.go' -not -path "./vendor/*") GOLIST := go list $(PKG)/... | grep -v '/vendor/' @@ -64,10 +65,10 @@ mod-check: build: @$(call print, "Building debug loop and loopd.") - $(GOBUILD) -o loop-debug $(LDFLAGS) $(PKG)/cmd/loop - $(GOBUILD) -o loopd-debug $(LDFLAGS) $(PKG)/cmd/loopd + $(GOBUILD) -tags="$(DEV_TAGS)" -o loop-debug $(LDFLAGS) $(PKG)/cmd/loop + $(GOBUILD) -tags="$(DEV_TAGS)" -o loopd-debug $(LDFLAGS) $(PKG)/cmd/loopd install: @$(call print, "Installing loop and loopd.") - $(GOINSTALL) $(LDFLAGS) $(PKG)/cmd/loop - $(GOINSTALL) $(LDFLAGS) $(PKG)/cmd/loopd + $(GOINSTALL) -tags="${tags}" $(LDFLAGS) $(PKG)/cmd/loop + $(GOINSTALL) -tags="${tags}" $(LDFLAGS) $(PKG)/cmd/loopd diff --git a/liquidity/liquidity.go b/liquidity/liquidity.go index 2688bf8..42d3437 100644 --- a/liquidity/liquidity.go +++ b/liquidity/liquidity.go @@ -453,6 +453,17 @@ func (m *Manager) autoloop(ctx context.Context) error { return nil } +// ForceAutoLoop force-ticks our auto-out ticker. +func (m *Manager) ForceAutoLoop(ctx context.Context) error { + select { + case m.cfg.AutoOutTicker.Force <- m.cfg.Clock.Now(): + return nil + + case <-ctx.Done(): + return ctx.Err() + } +} + // SuggestSwaps returns a set of swap suggestions based on our current liquidity // balance for the set of rules configured for the manager, failing if there are // no rules set. It takes an autoOut boolean that indicates whether the diff --git a/loopd/daemon.go b/loopd/daemon.go index 4295c10..471a344 100644 --- a/loopd/daemon.go +++ b/loopd/daemon.go @@ -197,6 +197,9 @@ func (d *Daemon) startWebServers() error { d.grpcServer = grpc.NewServer(serverOpts...) looprpc.RegisterSwapClientServer(d.grpcServer, d) + // Register our debug server if it is compiled in. + d.registerDebugServer() + // Next, start the gRPC server listening for HTTP/2 connections. log.Infof("Starting gRPC listener") serverTLSCfg, restClientCreds, err := getTLSConfig(d.cfg) @@ -355,6 +358,7 @@ func (d *Daemon) initialize() error { // Now finally fully initialize the swap client RPC server instance. d.swapClientServer = swapClientServer{ + network: lndclient.Network(d.cfg.Network), impl: swapclient, liquidityMgr: getLiquidityManager(swapclient), lnd: &d.lnd.LndServices, diff --git a/loopd/macaroons.go b/loopd/macaroons.go index cec4f09..5d8117a 100644 --- a/loopd/macaroons.go +++ b/loopd/macaroons.go @@ -164,7 +164,8 @@ func (d *Daemon) startMacaroonService() error { // We only generate one default macaroon that contains all // existing permissions (equivalent to the admin.macaroon in // lnd). Custom macaroons can be created through the bakery - // RPC. + // RPC. Add our debug permissions if required. + allPermissions = append(allPermissions, debugPermissions...) loopMac, err := d.macaroonService.Oven.NewMacaroon( ctx, bakery.LatestVersion, nil, allPermissions..., ) @@ -196,6 +197,12 @@ func (d *Daemon) stopMacaroonService() error { // macaroonInterceptor creates gRPC server options with the macaroon security // interceptors. func (d *Daemon) macaroonInterceptor() []grpc.ServerOption { + // Add our debug permissions to our main set of required permissions + // if compiled in. + for endpoint, perm := range debugRequiredPermissions { + RequiredPermissions[endpoint] = perm + } + unaryInterceptor := d.macaroonService.UnaryServerInterceptor( RequiredPermissions, ) diff --git a/loopd/register_default.go b/loopd/register_default.go new file mode 100644 index 0000000..08f3572 --- /dev/null +++ b/loopd/register_default.go @@ -0,0 +1,14 @@ +// +build !dev + +package loopd + +import "gopkg.in/macaroon-bakery.v2/bakery" + +var ( + debugRequiredPermissions = map[string][]bakery.Op{} + debugPermissions []bakery.Op +) + +// registerDebugServer is our default debug server registration function, which +// excludes debug functionality. +func (d *Daemon) registerDebugServer() {} diff --git a/loopd/swapclient_server.go b/loopd/swapclient_server.go index 4c17f89..5b12ed9 100644 --- a/loopd/swapclient_server.go +++ b/loopd/swapclient_server.go @@ -34,6 +34,7 @@ const ( // swapClientServer implements the grpc service exposed by loopd. type swapClientServer struct { + network lndclient.Network impl *loop.Client liquidityMgr *liquidity.Manager lnd *lndclient.LndServices diff --git a/loopd/swapclient_server_debug.go b/loopd/swapclient_server_debug.go new file mode 100644 index 0000000..81182b3 --- /dev/null +++ b/loopd/swapclient_server_debug.go @@ -0,0 +1,48 @@ +// +build dev + +package loopd + +import ( + "context" + "fmt" + + "gopkg.in/macaroon-bakery.v2/bakery" + + "github.com/lightninglabs/lndclient" + "github.com/lightninglabs/loop/looprpc" +) + +var ( + debugRequiredPermissions = map[string][]bakery.Op{ + "/looprpc.Debug/ForceAutoLoop": {{ + Entity: "debug", + Action: "write", + }}, + } + + debugPermissions = []bakery.Op{ + { + Entity: "debug", + Action: "write", + }, + } +) + +// registerDebugServer registers the debug server. +func (d *Daemon) registerDebugServer() { + looprpc.RegisterDebugServer(d.grpcServer, d) +} + +// ForceAutoLoop triggers our liquidity manager to dispatch an automated swap, +// if one is suggested. This endpoint is only for testing purposes and cannot be +// used on mainnet. +func (s *swapClientServer) ForceAutoLoop(ctx context.Context, + _ *looprpc.ForceAutoLoopRequest) (*looprpc.ForceAutoLoopResponse, error) { + + if s.network == lndclient.NetworkMainnet { + return nil, fmt.Errorf("force autoloop not allowed on mainnet") + } + + err := s.liquidityMgr.ForceAutoLoop(ctx) + return &looprpc.ForceAutoLoopResponse{}, err +} diff --git a/looprpc/debug.pb.go b/looprpc/debug.pb.go new file mode 100644 index 0000000..3f14ca9 --- /dev/null +++ b/looprpc/debug.pb.go @@ -0,0 +1,194 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: debug.proto + +package looprpc + +import ( + context "context" + fmt "fmt" + proto "github.com/golang/protobuf/proto" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + math "math" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package + +type ForceAutoLoopRequest struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ForceAutoLoopRequest) Reset() { *m = ForceAutoLoopRequest{} } +func (m *ForceAutoLoopRequest) String() string { return proto.CompactTextString(m) } +func (*ForceAutoLoopRequest) ProtoMessage() {} +func (*ForceAutoLoopRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_8d9d361be58531fb, []int{0} +} + +func (m *ForceAutoLoopRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ForceAutoLoopRequest.Unmarshal(m, b) +} +func (m *ForceAutoLoopRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ForceAutoLoopRequest.Marshal(b, m, deterministic) +} +func (m *ForceAutoLoopRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_ForceAutoLoopRequest.Merge(m, src) +} +func (m *ForceAutoLoopRequest) XXX_Size() int { + return xxx_messageInfo_ForceAutoLoopRequest.Size(m) +} +func (m *ForceAutoLoopRequest) XXX_DiscardUnknown() { + xxx_messageInfo_ForceAutoLoopRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_ForceAutoLoopRequest proto.InternalMessageInfo + +type ForceAutoLoopResponse struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ForceAutoLoopResponse) Reset() { *m = ForceAutoLoopResponse{} } +func (m *ForceAutoLoopResponse) String() string { return proto.CompactTextString(m) } +func (*ForceAutoLoopResponse) ProtoMessage() {} +func (*ForceAutoLoopResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_8d9d361be58531fb, []int{1} +} + +func (m *ForceAutoLoopResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ForceAutoLoopResponse.Unmarshal(m, b) +} +func (m *ForceAutoLoopResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ForceAutoLoopResponse.Marshal(b, m, deterministic) +} +func (m *ForceAutoLoopResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_ForceAutoLoopResponse.Merge(m, src) +} +func (m *ForceAutoLoopResponse) XXX_Size() int { + return xxx_messageInfo_ForceAutoLoopResponse.Size(m) +} +func (m *ForceAutoLoopResponse) XXX_DiscardUnknown() { + xxx_messageInfo_ForceAutoLoopResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_ForceAutoLoopResponse proto.InternalMessageInfo + +func init() { + proto.RegisterType((*ForceAutoLoopRequest)(nil), "looprpc.ForceAutoLoopRequest") + proto.RegisterType((*ForceAutoLoopResponse)(nil), "looprpc.ForceAutoLoopResponse") +} + +func init() { proto.RegisterFile("debug.proto", fileDescriptor_8d9d361be58531fb) } + +var fileDescriptor_8d9d361be58531fb = []byte{ + // 117 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x4e, 0x49, 0x4d, 0x2a, + 0x4d, 0xd7, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0xcf, 0xc9, 0xcf, 0x2f, 0x28, 0x2a, 0x48, + 0x56, 0x12, 0xe3, 0x12, 0x71, 0xcb, 0x2f, 0x4a, 0x4e, 0x75, 0x2c, 0x2d, 0xc9, 0xf7, 0xc9, 0xcf, + 0x2f, 0x08, 0x4a, 0x2d, 0x2c, 0x4d, 0x2d, 0x2e, 0x51, 0x12, 0xe7, 0x12, 0x45, 0x13, 0x2f, 0x2e, + 0xc8, 0xcf, 0x2b, 0x4e, 0x35, 0x8a, 0xe4, 0x62, 0x75, 0x01, 0x19, 0x24, 0x14, 0xc0, 0xc5, 0x8b, + 0xa2, 0x42, 0x48, 0x56, 0x0f, 0x6a, 0xa8, 0x1e, 0x36, 0x13, 0xa5, 0xe4, 0x70, 0x49, 0x43, 0x0c, + 0x56, 0x62, 0x48, 0x62, 0x03, 0xbb, 0xcd, 0x18, 0x10, 0x00, 0x00, 0xff, 0xff, 0x39, 0x7a, 0x3c, + 0x6b, 0xaa, 0x00, 0x00, 0x00, +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConn + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion4 + +// DebugClient is the client API for Debug service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. +type DebugClient interface { + // + //ForceAutoLoop is intended for *testing purposes only* and will not work on + //mainnet. This endpoint ticks our autoloop timer, triggering automated + //dispatch of a swap if one is suggested. + ForceAutoLoop(ctx context.Context, in *ForceAutoLoopRequest, opts ...grpc.CallOption) (*ForceAutoLoopResponse, error) +} + +type debugClient struct { + cc *grpc.ClientConn +} + +func NewDebugClient(cc *grpc.ClientConn) DebugClient { + return &debugClient{cc} +} + +func (c *debugClient) ForceAutoLoop(ctx context.Context, in *ForceAutoLoopRequest, opts ...grpc.CallOption) (*ForceAutoLoopResponse, error) { + out := new(ForceAutoLoopResponse) + err := c.cc.Invoke(ctx, "/looprpc.Debug/ForceAutoLoop", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// DebugServer is the server API for Debug service. +type DebugServer interface { + // + //ForceAutoLoop is intended for *testing purposes only* and will not work on + //mainnet. This endpoint ticks our autoloop timer, triggering automated + //dispatch of a swap if one is suggested. + ForceAutoLoop(context.Context, *ForceAutoLoopRequest) (*ForceAutoLoopResponse, error) +} + +// UnimplementedDebugServer can be embedded to have forward compatible implementations. +type UnimplementedDebugServer struct { +} + +func (*UnimplementedDebugServer) ForceAutoLoop(ctx context.Context, req *ForceAutoLoopRequest) (*ForceAutoLoopResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ForceAutoLoop not implemented") +} + +func RegisterDebugServer(s *grpc.Server, srv DebugServer) { + s.RegisterService(&_Debug_serviceDesc, srv) +} + +func _Debug_ForceAutoLoop_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ForceAutoLoopRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DebugServer).ForceAutoLoop(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/looprpc.Debug/ForceAutoLoop", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DebugServer).ForceAutoLoop(ctx, req.(*ForceAutoLoopRequest)) + } + return interceptor(ctx, in, info, handler) +} + +var _Debug_serviceDesc = grpc.ServiceDesc{ + ServiceName: "looprpc.Debug", + HandlerType: (*DebugServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "ForceAutoLoop", + Handler: _Debug_ForceAutoLoop_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "debug.proto", +} diff --git a/looprpc/debug.proto b/looprpc/debug.proto new file mode 100644 index 0000000..9358023 --- /dev/null +++ b/looprpc/debug.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; + +package looprpc; + +/* +Debug is a service that exposes endpoints intended for testing purposes. These +endpoints should not operate on mainnet, and should only be included if loop is +built with the dev build tag. +*/ +service Debug { + /* + ForceAutoLoop is intended for *testing purposes only* and will not work on + mainnet. This endpoint ticks our autoloop timer, triggering automated + dispatch of a swap if one is suggested. + */ + rpc ForceAutoLoop(ForceAutoLoopRequest) returns (ForceAutoLoopResponse){} +} + +message ForceAutoLoopRequest { +} + +message ForceAutoLoopResponse { +}