Merge pull request #3282 from halseth/mobile-rpcs

[mobile] Mobile RPCs
This commit is contained in:
Olaoluwa Osuntokun 2019-09-09 02:23:26 -07:00 committed by GitHub
commit 918fd9e8ff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 368 additions and 57 deletions

4
.gitignore vendored

@ -43,6 +43,10 @@ cmd/cmd
cmd/lncli/lncli
# Files from mobile build.
mobile/build
mobile/*_generated.go
# vim
*.swp

@ -1,5 +1,6 @@
PKG := github.com/lightningnetwork/lnd
ESCPKG := github.com\/lightningnetwork\/lnd
MOBILE_PKG := $(PKG)/mobile
BTCD_PKG := github.com/btcsuite/btcd
GOVERALLS_PKG := github.com/mattn/goveralls
@ -8,11 +9,17 @@ GOACC_PKG := github.com/ory/go-acc
GO_BIN := ${GOPATH}/bin
BTCD_BIN := $(GO_BIN)/btcd
GOMOBILE_BIN := GO111MODULE=off $(GO_BIN)/gomobile
GOVERALLS_BIN := $(GO_BIN)/goveralls
LINT_BIN := $(GO_BIN)/golangci-lint
GOACC_BIN := $(GO_BIN)/go-acc
BTCD_DIR :=${GOPATH}/src/$(BTCD_PKG)
MOBILE_BUILD_DIR :=${GOPATH}/src/$(MOBILE_PKG)/build
IOS_BUILD_DIR := $(MOBILE_BUILD_DIR)/ios
IOS_BUILD := $(IOS_BUILD_DIR)/Lndmobile.framework
ANDROID_BUILD_DIR := $(MOBILE_BUILD_DIR)/android
ANDROID_BUILD := $(ANDROID_BUILD_DIR)/Lndmobile.aar
COMMIT := $(shell git describe --abbrev=40 --dirty)
LDFLAGS := -ldflags "-X $(PKG)/build.Commit=$(COMMIT)"
@ -170,6 +177,26 @@ rpc:
@$(call print, "Compiling protos.")
cd ./lnrpc; ./gen_protos.sh
mobile-rpc:
@$(call print, "Creating mobile RPC from protos.")
cd ./mobile; ./gen_bindings.sh
vendor:
@$(call print, "Re-creating vendor directory.")
rm -r vendor/; GO111MODULE=on go mod vendor
ios: vendor mobile-rpc
@$(call print, "Building iOS framework ($(IOS_BUILD)).")
mkdir -p $(IOS_BUILD_DIR)
$(GOMOBILE_BIN) bind -target=ios -tags="ios $(DEV_TAGS) autopilotrpc experimental" $(LDFLAGS) -v -o $(IOS_BUILD) $(MOBILE_PKG)
android: vendor mobile-rpc
@$(call print, "Building Android library ($(ANDROID_BUILD)).")
mkdir -p $(ANDROID_BUILD_DIR)
$(GOMOBILE_BIN) bind -target=android -tags="android $(DEV_TAGS) autopilotrpc experimental" $(LDFLAGS) -v -o $(ANDROID_BUILD) $(MOBILE_PKG)
mobile: ios android
clean:
@$(call print, "Cleaning source.$(NC)")
$(RM) ./lnd-debug ./lncli-debug
@ -199,4 +226,9 @@ clean:
lint \
list \
rpc \
mobile-rpc \
vendor \
ios \
android \
mobile \
clean

@ -11,7 +11,7 @@ import (
func main() {
// Call the "real" main in a nested manner so the defers will properly
// be executed in the case of a graceful shutdown.
if err := lnd.Main(); err != nil {
if err := lnd.Main(lnd.ListenerCfg{}); err != nil {
if e, ok := err.(*flags.Error); ok && e.Type == flags.ErrHelp {
} else {
fmt.Fprintln(os.Stderr, err)

138
lnd.go

@ -93,10 +93,29 @@ var (
}
)
// ListenerCfg is a wrapper around custom listeners that can be passed to lnd
// when calling its main method.
type ListenerCfg struct {
// WalletUnlocker can be set to the listener to use for the wallet
// unlocker. If nil a regular network listener will be created.
WalletUnlocker net.Listener
// RPCListener can be set to the listener to use for the RPC server. If
// nil a regular network listener will be created.
RPCListener net.Listener
}
// rpcListeners is a function type used for closures that fetches a set of RPC
// listeners for the current configuration, and the GRPC server options to use
// with these listeners. If no custom listeners are present, this should return
// normal listeners from the RPC endpoints defined in the config, and server
// options specifying TLS.
type rpcListeners func() ([]net.Listener, func(), []grpc.ServerOption, error)
// Main is the true entry point for lnd. This function is required since defers
// created in the top-level scope of a main method aren't executed if os.Exit()
// is called.
func Main() error {
func Main(lisCfg ListenerCfg) error {
// Load the configuration, and parse any command line options. This
// function will also set up logging properly.
loadedConfig, err := loadConfig()
@ -240,13 +259,60 @@ func Main() error {
// this information.
walletInitParams.Birthday = time.Now()
// getListeners is a closure that creates listeners from the
// RPCListeners defined in the config. It also returns a cleanup
// closure and the server options to use for the GRPC server.
getListeners := func() ([]net.Listener, func(), []grpc.ServerOption,
error) {
var grpcListeners []net.Listener
for _, grpcEndpoint := range cfg.RPCListeners {
// Start a gRPC server listening for HTTP/2
// connections.
lis, err := lncfg.ListenOnAddress(grpcEndpoint)
if err != nil {
ltndLog.Errorf("unable to listen on %s",
grpcEndpoint)
return nil, nil, nil, err
}
grpcListeners = append(grpcListeners, lis)
}
cleanup := func() {
for _, lis := range grpcListeners {
lis.Close()
}
}
return grpcListeners, cleanup, serverOpts, nil
}
// walletUnlockerListeners is a closure we'll hand to the wallet
// unlocker, that will be called when it needs listeners for its GPRC
// server.
walletUnlockerListeners := func() ([]net.Listener, func(),
[]grpc.ServerOption, error) {
// If we have chosen to start with a dedicated listener for the
// wallet unlocker, we return it directly, and empty server
// options to deactivate TLS.
// TODO(halseth): any point in adding TLS support for custom
// listeners?
if lisCfg.WalletUnlocker != nil {
return []net.Listener{lisCfg.WalletUnlocker}, func() {},
[]grpc.ServerOption{}, nil
}
// Otherwise we'll return the regular listeners.
return getListeners()
}
// We wait until the user provides a password over RPC. In case lnd is
// started with the --noseedbackup flag, we use the default password
// for wallet encryption.
if !cfg.NoSeedBackup {
params, err := waitForWalletPassword(
cfg.RPCListeners, cfg.RESTListeners, serverOpts,
restDialOpts, restProxyDest, tlsCfg,
cfg.RESTListeners, restDialOpts, restProxyDest, tlsCfg,
walletUnlockerListeners,
)
if err != nil {
err := fmt.Errorf("Unable to set up wallet password "+
@ -457,12 +523,31 @@ func Main() error {
}
defer atplManager.Stop()
// rpcListeners is a closure we'll hand to the rpc server, that will be
// called when it needs listeners for its GPRC server.
rpcListeners := func() ([]net.Listener, func(), []grpc.ServerOption,
error) {
// If we have chosen to start with a dedicated listener for the
// rpc server, we return it directly, and empty server options
// to deactivate TLS.
// TODO(halseth): any point in adding TLS support for custom
// listeners?
if lisCfg.RPCListener != nil {
return []net.Listener{lisCfg.RPCListener}, func() {},
[]grpc.ServerOption{}, nil
}
// Otherwise we'll return the regular listeners.
return getListeners()
}
// Initialize, and register our implementation of the gRPC interface
// exported by the rpcServer.
rpcServer, err := newRPCServer(
server, macaroonService, cfg.SubRPCServers, serverOpts,
restDialOpts, restProxyDest, atplManager, server.invoices,
tower, tlsCfg,
server, macaroonService, cfg.SubRPCServers, restDialOpts,
restProxyDest, atplManager, server.invoices, tower, tlsCfg,
rpcListeners,
)
if err != nil {
err := fmt.Errorf("Unable to create RPC server: %v", err)
@ -880,9 +965,18 @@ type WalletUnlockParams struct {
// waitForWalletPassword will spin up gRPC and REST endpoints for the
// WalletUnlocker server, and block until a password is provided by
// the user to this RPC server.
func waitForWalletPassword(grpcEndpoints, restEndpoints []net.Addr,
serverOpts []grpc.ServerOption, restDialOpts []grpc.DialOption,
restProxyDest string, tlsConf *tls.Config) (*WalletUnlockParams, error) {
func waitForWalletPassword(restEndpoints []net.Addr,
restDialOpts []grpc.DialOption, restProxyDest string,
tlsConf *tls.Config, getListeners rpcListeners) (
*WalletUnlockParams, error) {
// Start a gRPC server listening for HTTP/2 connections, solely used
// for getting the encryption password from the client.
listeners, cleanup, serverOpts, err := getListeners()
if err != nil {
return nil, err
}
defer cleanup()
// Set up a new PasswordService, which will listen for passwords
// provided over RPC.
@ -911,28 +1005,14 @@ func waitForWalletPassword(grpcEndpoints, restEndpoints []net.Addr,
// password is the last thing to be printed to the console.
var wg sync.WaitGroup
for _, grpcEndpoint := range grpcEndpoints {
// Start a gRPC server listening for HTTP/2 connections, solely
// used for getting the encryption password from the client.
lis, err := lncfg.ListenOnAddress(grpcEndpoint)
if err != nil {
ltndLog.Errorf(
"password RPC server unable to listen on %s",
grpcEndpoint,
)
return nil, err
}
defer lis.Close()
for _, lis := range listeners {
wg.Add(1)
go func() {
rpcsLog.Infof(
"password RPC server listening on %s",
lis.Addr(),
)
go func(lis net.Listener) {
rpcsLog.Infof("password RPC server listening on %s",
lis.Addr())
wg.Done()
grpcServer.Serve(lis)
}()
}(lis)
}
// Start a REST proxy for our gRPC server above.
@ -942,7 +1022,7 @@ func waitForWalletPassword(grpcEndpoints, restEndpoints []net.Addr,
mux := proxy.NewServeMux()
err := lnrpc.RegisterWalletUnlockerHandlerFromEndpoint(
err = lnrpc.RegisterWalletUnlockerHandlerFromEndpoint(
ctx, mux, restProxyDest, restDialOpts,
)
if err != nil {

55
mobile/README.md Normal file

@ -0,0 +1,55 @@
## Building mobile libraries
### Prerequisites
#### protoc
Install the dependencies for genarating protobuf definitions as stated in [lnrpc docs](
../lnrpc/README.md#generate-protobuf-definitions)
#### gomobile
Follow [gomobile](https://github.com/golang/go/wiki/Mobile) in order to intall `gomobile` and dependencies.
Remember to run `gomobile init` (otherwise the `lnd` build might just hang).
Note that `gomobile` only supports building projects from `GOPATH` at this point.
#### falafel
Install [`falafel`](https://github.com/halseth/falafel):
```
go get -u -v github.com/halseth/falafel
```
### Building `lnd` for iOS
```
make ios
```
### Building `lnd` for Android
```
make android
```
`make mobile` will build both iOS and Android libs.
### Libraries
After the build has succeeded, the libraries will be found in `mobile/build/ios/Lndmobile.framework` and `mobile/build/android/Lndmobile.aar`. Reference your platforms' SDK documentation for how to add the library to your project.
#### Generating proto definitions for your language.
In order to call the methods in the generated library, the serialized proto for the given RPC call must be provided. Similarly, the response will be a serialized proto.
In order to generate protobuf definitions for your language of choice, add the proto plugin to the `protoc` invocations found in [`gen_protos.sh`](../lnrpc/gen_protos.sh). For instance to generate protos for Swift, add `--swift_out=.` and run `make rpc`.
### Options
Similar to lnd, subservers can be conditionally compiled with the build by setting the tags argument:
```
make ios tags="routerrpc"
```
To support subservers that have APIs with name conflicts, pass the "prefix" flag. This will add the subserver name as a prefix to each method name:
```
make ios tags="routerrpc" prefix=1
```
### API docs
TODO(halseth)

61
mobile/bindings.go Normal file

@ -0,0 +1,61 @@
// +build ios android
package lndmobile
import (
"fmt"
"os"
"strings"
flags "github.com/jessevdk/go-flags"
"github.com/lightningnetwork/lnd"
)
// Start starts lnd in a new goroutine.
//
// extraArgs can be used to pass command line arguments to lnd that will
// override what is found in the config file. Example:
// extraArgs = "--bitcoin.testnet --lnddir=\"/tmp/folder name/\" --profile=5050"
func Start(extraArgs string, callback Callback) {
// Split the argument string on "--" to get separated command line
// arguments.
var splitArgs []string
for _, a := range strings.Split(extraArgs, "--") {
if a == "" {
continue
}
// Finally we prefix any non-empty string with --, and trim
// whitespace to mimic the regular command line arguments.
splitArgs = append(splitArgs, strings.TrimSpace("--"+a))
}
// Add the extra arguments to os.Args, as that will be parsed during
// startup.
os.Args = append(os.Args, splitArgs...)
// We call the main method with the custom in-memory listeners called
// by the mobile APIs, such that the grpc server will use these.
cfg := lnd.ListenerCfg{
WalletUnlocker: walletUnlockerLis,
RPCListener: lightningLis,
}
// Call the "real" main in a nested manner so the defers will properly
// be executed in the case of a graceful shutdown.
go func() {
if err := lnd.Main(cfg); err != nil {
if e, ok := err.(*flags.Error); ok &&
e.Type == flags.ErrHelp {
} else {
fmt.Fprintln(os.Stderr, err)
}
os.Exit(1)
}
}()
// TODO(halseth): callback when RPC server is actually running. Since
// the RPC server might take a while to start up, the client might
// assume it is ready to accept calls when this callback is sent, while
// it's not.
callback.OnResponse([]byte("started"))
}

64
mobile/gen_bindings.sh Executable file

@ -0,0 +1,64 @@
#!/bin/sh
mkdir -p build
# Check falafel version.
falafelVersion="0.5"
falafel=$(which falafel)
if [ $falafel ]
then
version=$($falafel -v)
if [ $version != $falafelVersion ]
then
echo "falafel version $falafelVersion required"
exit 1
fi
echo "Using plugin $falafel $version"
else
echo "falafel not found"
exit 1
fi
pkg="lndmobile"
target_pkg="github.com/lightningnetwork/lnd/lnrpc"
# Generate APIs by passing the parsed protos to the falafel plugin.
opts="package_name=$pkg,target_package=$target_pkg,listeners=lightning=lightningLis walletunlocker=walletUnlockerLis,mem_rpc=1"
protoc -I/usr/local/include -I. \
-I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
--plugin=protoc-gen-custom=$falafel\
--custom_out=./build \
--custom_opt="$opts" \
--proto_path=../lnrpc \
rpc.proto
# If prefix=1 is specified, prefix the generated methods with subserver name.
# This must be enabled to support subservers with name conflicts.
use_prefix="0"
if [[ $prefix = "1" ]]
then
echo "Prefixing methods with subserver name"
use_prefix="1"
fi
# Find all subservers.
for file in ../lnrpc/**/*.proto
do
DIRECTORY=$(dirname ${file})
tag=$(basename ${DIRECTORY})
build_tags="// +build $tag"
lis="lightningLis"
opts="package_name=$pkg,target_package=$target_pkg/$tag,build_tags=$build_tags,api_prefix=$use_prefix,defaultlistener=$lis"
echo "Generating mobile protos from ${file}, with build tag ${tag}"
protoc -I/usr/local/include -I. \
-I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
-I../lnrpc \
--plugin=protoc-gen-custom=$falafel \
--custom_out=./build \
--custom_opt="$opts" \
--proto_path=${DIRECTORY} \
${file}
done

13
mobile/sample_lnd.conf Normal file

@ -0,0 +1,13 @@
[Application Options]
debuglevel=info
no-macaroons=1
maxbackoff=2s
nolisten=1
[Routing]
routing.assumechanvalid=1
[Bitcoin]
bitcoin.active=1
bitcoin.testnet=1
bitcoin.node=neutrino

@ -8,6 +8,7 @@ import (
"fmt"
"io"
"math"
"net"
"net/http"
"sort"
"strings"
@ -409,6 +410,11 @@ type rpcServer struct {
// requests from.
grpcServer *grpc.Server
// listeners is a list of listeners to use when starting the grpc
// server. We make it configurable such that the grpc server can listen
// on custom interfaces.
listeners []net.Listener
// listenerCleanUp are a set of closures functions that will allow this
// main RPC server to clean up all the listening socket created for the
// server.
@ -442,10 +448,10 @@ var _ lnrpc.LightningServer = (*rpcServer)(nil)
// base level options passed to the grPC server. This typically includes things
// like requiring TLS, etc.
func newRPCServer(s *server, macService *macaroons.Service,
subServerCgs *subRPCServerConfigs, serverOpts []grpc.ServerOption,
restDialOpts []grpc.DialOption, restProxyDest string,
atpl *autopilot.Manager, invoiceRegistry *invoices.InvoiceRegistry,
tower *watchtower.Standalone, tlsCfg *tls.Config) (*rpcServer, error) {
subServerCgs *subRPCServerConfigs, restDialOpts []grpc.DialOption,
restProxyDest string, atpl *autopilot.Manager,
invoiceRegistry *invoices.InvoiceRegistry, tower *watchtower.Standalone,
tlsCfg *tls.Config, getListeners rpcListeners) (*rpcServer, error) {
// Set up router rpc backend.
channelGraph := s.chanDB.ChannelGraph()
@ -570,6 +576,12 @@ func newRPCServer(s *server, macService *macaroons.Service,
strmInterceptors, errorLogStreamServerInterceptor(rpcsLog),
)
// Get the listeners and server options to use for this rpc server.
listeners, cleanup, serverOpts, err := getListeners()
if err != nil {
return nil, err
}
// If any interceptors have been set up, add them to the server options.
if len(unaryInterceptors) != 0 && len(strmInterceptors) != 0 {
chainedUnary := grpc_middleware.WithUnaryServerChain(
@ -585,14 +597,16 @@ func newRPCServer(s *server, macService *macaroons.Service,
// gRPC server, and register the main lnrpc server along side.
grpcServer := grpc.NewServer(serverOpts...)
rootRPCServer := &rpcServer{
restDialOpts: restDialOpts,
restProxyDest: restProxyDest,
subServers: subServers,
tlsCfg: tlsCfg,
grpcServer: grpcServer,
server: s,
routerBackend: routerBackend,
quit: make(chan struct{}, 1),
restDialOpts: restDialOpts,
listeners: listeners,
listenerCleanUp: []func(){cleanup},
restProxyDest: restProxyDest,
subServers: subServers,
tlsCfg: tlsCfg,
grpcServer: grpcServer,
server: s,
routerBackend: routerBackend,
quit: make(chan struct{}, 1),
}
lnrpc.RegisterLightningServer(grpcServer, rootRPCServer)
@ -632,23 +646,11 @@ func (r *rpcServer) Start() error {
// With all the sub-servers started, we'll spin up the listeners for
// the main RPC server itself.
for _, listener := range cfg.RPCListeners {
lis, err := lncfg.ListenOnAddress(listener)
if err != nil {
ltndLog.Errorf(
"RPC server unable to listen on %s", listener,
)
return err
}
r.listenerCleanUp = append(r.listenerCleanUp, func() {
lis.Close()
})
go func() {
for _, lis := range r.listeners {
go func(lis net.Listener) {
rpcsLog.Infof("RPC server listening on %s", lis.Addr())
r.grpcServer.Serve(lis)
}()
}(lis)
}
// If Prometheus monitoring is enabled, start the Prometheus exporter.