├── .editorconfig ├── .github ├── pull_request_template.md └── workflows │ └── main.yml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── Makefile ├── README.md ├── basic_client.go ├── basic_client_test.go ├── chainkit_client.go ├── chainnotifier_client.go ├── go.mod ├── go.sum ├── invoices_client.go ├── lightning_client.go ├── lightning_client_test.go ├── lnd_services.go ├── lnd_services_test.go ├── log.go ├── macaroon_pouch.go ├── macaroon_recipes.go ├── macaroon_recipes_test.go ├── macaroon_service.go ├── macaroon_service_test.go ├── network.go ├── router_client.go ├── signer_client.go ├── state_client.go ├── testdata └── permissions.json ├── tools ├── Dockerfile ├── go.mod ├── go.sum └── tools.go ├── tx_utils.go ├── versioner_client.go └── walletkit_client.go /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # Top-most EditorConfig file. 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file. 7 | [*.md] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | max_line_length = 80 11 | 12 | # 8 space indentation for Golang code. 13 | [*.go] 14 | indent_style = tab 15 | indent_size = 8 16 | max_line_length = 80 17 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | #### Pull Request Checklist 2 | 3 | - [ ] PR is opened against correct version branch. 4 | - [ ] Version compatibility matrix in the README and minimal required version 5 | in `lnd_services.go` are updated. 6 | - [ ] Update `macaroon_recipes.go` if your PR adds a new method that is called 7 | differently than the RPC method it invokes. 8 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - "master" 7 | pull_request: 8 | branches: 9 | - "*" 10 | 11 | defaults: 12 | run: 13 | shell: bash 14 | 15 | env: 16 | GO_VERSION: 1.23.6 17 | 18 | jobs: 19 | build: 20 | name: build package, run linter 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: git checkout 24 | uses: actions/checkout@v4 25 | 26 | - name: setup go ${{ env.GO_VERSION }} 27 | uses: actions/setup-go@v5 28 | with: 29 | go-version: '~${{ env.GO_VERSION }}' 30 | 31 | - name: compile 32 | run: make build 33 | 34 | - name: lint 35 | run: make lint 36 | 37 | - name: unit-race 38 | run: make unit-race 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Vim files 15 | *.swp 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | .idea 20 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | # timeout for analysis 3 | timeout: 4m 4 | go: "1.23" 5 | 6 | linters-settings: 7 | govet: 8 | # Don't report about shadowed variables 9 | shadow: false 10 | gofmt: 11 | # simplify code: gofmt with `-s` option, true by default 12 | simplify: true 13 | tagliatelle: 14 | case: 15 | rules: 16 | json: snake 17 | whitespace: 18 | multi-func: true 19 | multi-if: true 20 | gosec: 21 | excludes: 22 | - G402 # Look for bad TLS connection settings. 23 | - G306 # Poor file permissions used when writing to a new file. 24 | - G115 # Integer overflow conversion. 25 | staticcheck: 26 | checks: ["-SA1019"] 27 | 28 | linters: 29 | enable-all: true 30 | disable: 31 | # Global variables are used in many places throughout the code base. 32 | - gochecknoglobals 33 | 34 | # Some lines are over 80 characters on purpose and we don't want to make them 35 | # even longer by marking them as 'nolint'. 36 | - lll 37 | 38 | # We want to allow short variable names. 39 | - varnamelen 40 | 41 | # We want to allow TODOs. 42 | - godox 43 | 44 | # We have long functions, especially in tests. Moving or renaming those would 45 | # trigger funlen problems that we may not want to solve at that time. 46 | - funlen 47 | 48 | # Disable for now as we haven't yet tuned the sensitivity to our codebase 49 | # yet. Enabling by default for example, would also force new contributors to 50 | # potentially extensively refactor code, when they want to smaller change to 51 | # land. 52 | - gocyclo 53 | - gocognit 54 | - cyclop 55 | 56 | # Instances of table driven tests that don't pre-allocate shouldn't trigger 57 | # the linter. 58 | - prealloc 59 | 60 | # Init functions are used by loggers throughout the codebase. 61 | - gochecknoinits 62 | 63 | # Causes stack overflow, see https://github.com/polyfloyd/go-errorlint/issues/19. 64 | - errorlint 65 | 66 | # New linters that need a code adjustment first. 67 | - wrapcheck 68 | - nolintlint 69 | - paralleltest 70 | - tparallel 71 | - testpackage 72 | - gofumpt 73 | - gomoddirectives 74 | - ireturn 75 | - maintidx 76 | - nlreturn 77 | - dogsled 78 | - gci 79 | - containedctx 80 | - contextcheck 81 | - errname 82 | - err113 83 | - mnd 84 | - noctx 85 | - nestif 86 | - wsl 87 | - exhaustive 88 | - forcetypeassert 89 | - nilerr 90 | - nilnil 91 | - stylecheck 92 | - thelper 93 | - exhaustruct 94 | - importas 95 | - interfacebloat 96 | - protogetter 97 | - revive 98 | - depguard 99 | - mnd 100 | - perfsprint 101 | - inamedparam 102 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Lightning Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PKG := github.com/lightninglabs/lndclient 2 | 3 | TOOLS_DIR := tools 4 | 5 | GOIMPORTS_PKG := github.com/rinchsan/gosimports/cmd/gosimports 6 | 7 | GO_BIN := ${GOPATH}/bin 8 | GOIMPORTS_BIN := $(GO_BIN)/gosimports 9 | 10 | GOBUILD := go build -v 11 | GOINSTALL := go install -v 12 | GOTEST := go test -v 13 | 14 | GOFILES_NOVENDOR = $(shell find . -type f -name '*.go' -not -path "./vendor/*") 15 | GOLIST := go list -deps $(PKG)/... | grep '$(PKG)'| grep -v '/vendor/' 16 | 17 | COMMIT := $(shell git describe --abbrev=40 --dirty) 18 | LDFLAGS := -X $(PKG).Commit=$(COMMIT) 19 | 20 | RM := rm -f 21 | CP := cp 22 | MAKE := make 23 | XARGS := xargs -L 1 24 | 25 | # Linting uses a lot of memory, so keep it under control by limiting the number 26 | # of workers if requested. 27 | ifneq ($(workers),) 28 | LINT_WORKERS = --concurrency=$(workers) 29 | endif 30 | 31 | DOCKER_TOOLS = docker run -v $$(pwd):/build lndclient-tools 32 | 33 | GREEN := "\\033[0;32m" 34 | NC := "\\033[0m" 35 | define print 36 | echo $(GREEN)$1$(NC) 37 | endef 38 | 39 | default: build 40 | 41 | all: build check install 42 | 43 | # ============ 44 | # DEPENDENCIES 45 | # ============ 46 | $(GOIMPORTS_BIN): 47 | @$(call print, "Installing goimports.") 48 | cd $(TOOLS_DIR); go install -trimpath $(GOIMPORTS_PKG) 49 | 50 | # ============ 51 | # INSTALLATION 52 | # ============ 53 | 54 | build: 55 | @$(call print, "Building lndclient.") 56 | $(GOBUILD) -ldflags="$(LDFLAGS)" $(PKG) 57 | 58 | docker-tools: 59 | @$(call print, "Building tools docker image.") 60 | docker build -q -t lndclient-tools $(TOOLS_DIR) 61 | 62 | # ======= 63 | # TESTING 64 | # ======= 65 | 66 | check: unit 67 | 68 | unit: 69 | @$(call print, "Running unit tests.") 70 | $(GOTEST) ./... 71 | 72 | unit-race: 73 | @$(call print, "Running unit race tests.") 74 | env CGO_ENABLED=1 GORACE="history_size=7 halt_on_errors=1" $(GOTEST) -race ./... 75 | 76 | # ========= 77 | # UTILITIES 78 | # ========= 79 | fmt: $(GOIMPORTS_BIN) 80 | @$(call print, "Fixing imports.") 81 | gosimports -w $(GOFILES_NOVENDOR) 82 | @$(call print, "Formatting source.") 83 | gofmt -l -w -s $(GOFILES_NOVENDOR) 84 | 85 | lint: docker-tools 86 | @$(call print, "Linting source.") 87 | $(DOCKER_TOOLS) golangci-lint run -v $(LINT_WORKERS) 88 | 89 | .PHONY: default \ 90 | build \ 91 | unit \ 92 | unit-race \ 93 | fmt \ 94 | lint 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Golang client library for lnd 2 | 3 | `lndclient` is a golang native wrapper for `lnd`'s gRPC interface. 4 | 5 | ## Compatibility matrix 6 | 7 | This library depends heavily on `lnd` for obvious reasons. To support backward 8 | compatibility with older versions of `lnd`, we use different branches for 9 | different versions. There are two "levels" of depending on a version of 10 | `lnd`: 11 | - Code level dependency: This is the version of `lnd` that is pulled in when 12 | compiling `lndclient`. It is defined in `go.mod`. This usually is the latest 13 | released version of `lnd`, because its RPC definitions are kept backward 14 | compatible. This means that a new field added in the latest version of `lnd` 15 | might already be available in `lndclient`'s code, but whether or not that 16 | field will actually be set at run time is dependent on the actual version of 17 | `lnd` that's being connected to. 18 | - RPC level dependency: This is defined in `minimalCompatibleVersion` in 19 | [`lnd_services.go`](lnd_services.go). When connecting to `lnd`, the version 20 | returned by its version service is checked and if it doesn't meet the minimal 21 | required version defined in `lnd_services.go`, an error will be returned. 22 | Users of `lndclient` can also overwrite this minimum required version when 23 | creating a new client. 24 | 25 | The current compatibility matrix reads as follows: 26 | 27 | | `lndclient` git tag | `lnd` version in `go.mod` | minimum required `lnd` version | 28 | |---------------------------------------------------------------------------------------|---------------------------|--------------------------------| 29 | | `master` / [`v0.18.5-13`](https://github.com/lightninglabs/lndclient/blob/v0.18.5-13) | `v0.18.5-beta` | `v0.18.5-beta` | 30 | 31 | 32 | By default, `lndclient` requires (and enforces) the following RPC subservers to 33 | be active in `lnd`: 34 | - `signrpc` 35 | - `walletrpc` 36 | - `chainrpc` 37 | - `invoicesrpc` 38 | 39 | ## Branch strategy 40 | 41 | We follow the following strategy to maintain different versions of this library 42 | that have different `lnd` compatibilities: 43 | 44 | 1. The `master` is always backward compatible with the last major version. 45 | 2. We create branches for all minor versions and future major versions and merge PRs to those branches, if the features require that version to work. 46 | 3. We rebase the branches if needed and use tags to track versions that we depend on in other projects. 47 | 4. Once a new major version of `lnd` is final, all branches of minor versions lower than that are merged into master. 48 | -------------------------------------------------------------------------------- /basic_client.go: -------------------------------------------------------------------------------- 1 | package lndclient 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/lightningnetwork/lnd/lncfg" 10 | "github.com/lightningnetwork/lnd/lnrpc" 11 | "github.com/lightningnetwork/lnd/macaroons" 12 | "google.golang.org/grpc" 13 | "google.golang.org/grpc/credentials" 14 | macaroon "gopkg.in/macaroon.v2" 15 | ) 16 | 17 | // BasicClientOption is a functional option argument that allows adding 18 | // arbitrary lnd basic client configuration overrides, without forcing existing 19 | // users of NewBasicClient to update their invocation. These are always 20 | // processed in order, with later options overriding earlier ones. 21 | type BasicClientOption func(*basicClientOptions) 22 | 23 | // basicClientOptions is a set of options that can configure the lnd client 24 | // returned by NewBasicClient. 25 | type basicClientOptions struct { 26 | macFilename string 27 | tlsData string 28 | macData string 29 | insecure bool 30 | systemCerts bool 31 | } 32 | 33 | // defaultBasicClientOptions returns a basicClientOptions set to lnd basic 34 | // client defaults. 35 | func defaultBasicClientOptions() *basicClientOptions { 36 | return &basicClientOptions{ 37 | macFilename: string(AdminServiceMac), 38 | } 39 | } 40 | 41 | // MacFilename is a basic client option that sets the name of the macaroon file 42 | // to use. 43 | func MacFilename(macFilename string) BasicClientOption { 44 | return func(bc *basicClientOptions) { 45 | bc.macFilename = macFilename 46 | } 47 | } 48 | 49 | // TLSData is a basic client option that sets TLS data (encoded in PEM format) 50 | // directly instead of loading them from a file. 51 | func TLSData(tlsData string) BasicClientOption { 52 | return func(bc *basicClientOptions) { 53 | bc.tlsData = tlsData 54 | } 55 | } 56 | 57 | // MacaroonData is a basic client option that sets macaroon data (encoded as hex 58 | // string) directly instead of loading it from a file. 59 | func MacaroonData(macData string) BasicClientOption { 60 | return func(bc *basicClientOptions) { 61 | bc.macData = macData 62 | } 63 | } 64 | 65 | // Insecure allows the basic client to establish an insecure (non-TLS) 66 | // connection to the RPC server. 67 | func Insecure() BasicClientOption { 68 | return func(bc *basicClientOptions) { 69 | bc.insecure = true 70 | } 71 | } 72 | 73 | // SystemCerts instructs the basic client to use the system's certificate trust 74 | // store for verifying the TLS certificate of the RPC server. 75 | func SystemCerts() BasicClientOption { 76 | return func(bc *basicClientOptions) { 77 | bc.systemCerts = true 78 | } 79 | } 80 | 81 | // applyBasicClientOptions updates a basicClientOptions set with functional 82 | // options. 83 | func (bc *basicClientOptions) applyBasicClientOptions( 84 | options ...BasicClientOption) { 85 | 86 | for _, option := range options { 87 | option(bc) 88 | } 89 | } 90 | 91 | // NewBasicClient creates a new basic gRPC client to lnd. We call this client 92 | // "basic" as it falls back to expected defaults if the arguments aren't 93 | // provided. 94 | func NewBasicClient(lndHost, tlsPath, macDir, network string, 95 | basicOptions ...BasicClientOption) (lnrpc.LightningClient, error) { 96 | 97 | conn, err := NewBasicConn( 98 | lndHost, tlsPath, macDir, network, basicOptions..., 99 | ) 100 | if err != nil { 101 | return nil, err 102 | } 103 | 104 | return lnrpc.NewLightningClient(conn), nil 105 | } 106 | 107 | // NewBasicConn creates a new basic gRPC connection to lnd. We call this 108 | // connection "basic" as it falls back to expected defaults if the arguments 109 | // aren't provided. 110 | func NewBasicConn(lndHost string, tlsPath, macDir, network string, 111 | basicOptions ...BasicClientOption) (*grpc.ClientConn, error) { 112 | 113 | creds, mac, err := parseTLSAndMacaroon( 114 | tlsPath, macDir, network, basicOptions..., 115 | ) 116 | if err != nil { 117 | return nil, err 118 | } 119 | 120 | // Now we append the macaroon credentials to the dial options. 121 | cred, err := macaroons.NewMacaroonCredential(mac) 122 | if err != nil { 123 | return nil, fmt.Errorf("error creating macaroon credential: %v", 124 | err) 125 | } 126 | 127 | // Create a dial options array. 128 | opts := []grpc.DialOption{ 129 | grpc.WithTransportCredentials(creds), 130 | grpc.WithPerRPCCredentials(cred), 131 | grpc.WithDefaultCallOptions(maxMsgRecvSize), 132 | } 133 | 134 | // We need to use a custom dialer so we can also connect to unix sockets 135 | // and not just TCP addresses. 136 | opts = append( 137 | opts, grpc.WithContextDialer( 138 | lncfg.ClientAddressDialer(defaultRPCPort), 139 | ), 140 | ) 141 | conn, err := grpc.Dial(lndHost, opts...) 142 | if err != nil { 143 | return nil, fmt.Errorf("unable to connect to RPC server: %v", 144 | err) 145 | } 146 | 147 | return conn, nil 148 | } 149 | 150 | // parseTLSAndMacaroon looks to see if the TLS certificate and macaroon were 151 | // passed in as a path or as straight-up data, and processes it accordingly, so 152 | // it can be passed into grpc to establish a connection with LND. 153 | func parseTLSAndMacaroon(tlsPath, macDir, network string, 154 | basicOptions ...BasicClientOption) (credentials.TransportCredentials, 155 | *macaroon.Macaroon, error) { 156 | 157 | // Starting with the set of default options, we'll apply any specified 158 | // functional options to the basic client. 159 | bco := defaultBasicClientOptions() 160 | bco.applyBasicClientOptions(basicOptions...) 161 | 162 | creds, err := GetTLSCredentials( 163 | bco.tlsData, tlsPath, bco.insecure, bco.systemCerts, 164 | ) 165 | if err != nil { 166 | return nil, nil, err 167 | } 168 | 169 | var macBytes []byte 170 | mac := &macaroon.Macaroon{} 171 | if bco.macData != "" { 172 | macBytes, err = hex.DecodeString(bco.macData) 173 | if err != nil { 174 | return nil, nil, err 175 | } 176 | } else { 177 | if macDir == "" { 178 | macDir = filepath.Join( 179 | defaultLndDir, defaultDataDir, defaultChainSubDir, 180 | "bitcoin", network, 181 | ) 182 | } 183 | 184 | macPath := filepath.Join(macDir, bco.macFilename) 185 | 186 | // Load the specified macaroon file. 187 | macBytes, err = os.ReadFile(macPath) 188 | if err != nil { 189 | return nil, nil, err 190 | } 191 | } 192 | 193 | if err = mac.UnmarshalBinary(macBytes); err != nil { 194 | return nil, nil, fmt.Errorf("unable to decode macaroon: %v", 195 | err) 196 | } 197 | 198 | return creds, mac, nil 199 | } 200 | -------------------------------------------------------------------------------- /basic_client_test.go: -------------------------------------------------------------------------------- 1 | package lndclient 2 | 3 | import ( 4 | "encoding/hex" 5 | "os" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | // Tests that NewBasicConn works correctly when macaroon and TLS certificate 12 | // data are passed in directly instead of being supplied as file paths. 13 | func TestParseTLSAndMacaroon(t *testing.T) { 14 | tlsData := `-----BEGIN CERTIFICATE----- 15 | MIIDhzCCAm+gAwIBAgIUEkmdMOVPL92AwgsSYFFBvz4ilmUwDQYJKoZIhvcNAQEL 16 | BQAwUzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk1OMRQwEgYDVQQHDAtNaW5uZWFw 17 | b2xpczEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMB4XDTIxMDQy 18 | MzA2NDkyNVoXDTIxMDUyMzA2NDkyNVowUzELMAkGA1UEBhMCVVMxCzAJBgNVBAgM 19 | Ak1OMRQwEgYDVQQHDAtNaW5uZWFwb2xpczEhMB8GA1UECgwYSW50ZXJuZXQgV2lk 20 | Z2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnK21 21 | qJmWWs4Nwz2f2ZbTsDxJAumgDJdZ9JKsJBrqjFf7+25ip+1hIB15P1UHHPhtW5Yp 22 | P9Xm50z8W2RP2pHyCFB09cwKgdqPsS8Q2tzr5DINt+eNYa5JpxnWXM5ZqmYD7Zg0 23 | wSMVW3FuAWFpjlzNWs/UHSuDShiQLoMhl2xAjiGSsHbY9plV438/kypSKS+7wjxe 24 | 0TJaTv/kWlHhQkXvnLqIMhD8J+ScGVSSk0OFgWiRmcCGDsLZgEGklHklC7ZKrr+Q 25 | Am2MGbvUaGuwW+R5d2ZaQRbQ5UVhHcna2MxUn6MzSjbEhpIsMKZoYVXCb0GFObcq 26 | UsLUOrIqpIyngd4G9wIDAQABo1MwUTAdBgNVHQ4EFgQU0lZJ2gp/RM79oAegXr/H 27 | sU+GU3YwHwYDVR0jBBgwFoAU0lZJ2gp/RM79oAegXr/HsU+GU3YwDwYDVR0TAQH/ 28 | BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAly744gq/LPuL0EnEbfxXrVqmvWh6 29 | t9kNljXybVjQNTZ00e4zGknOA3VM29JWOEYyQ7ut/tP+kquWLdfOq/Lehe7vnBSn 30 | lPR6IYbba9ck5AvPZgGG9fEncKxeUoI0ltI/luycmWL7Eb9j3128diIwljf9JXNT 31 | I/LThs8Nl5RSiMOuGer0e934vLlZlrEEI4rWs3DKK56WjrMeVf5dhvYK44usNwUh 32 | vKgMVFsUeyLLTN0EuZjGoFdi3lfLQo3vRwLD6h/EDa5uWK14pZXDQ30+fT2RjuVD 33 | XhkpT5dliEGFLNe6OOgeWTU1JpEXfCud/GImtNMHQi4EDWQfvWuCNGhOoQ== 34 | -----END CERTIFICATE-----` 35 | 36 | macData := "0201047465737402067788991234560000062052d26ed139ea5af8" + 37 | "3e675500c4ccb2471f62191b745bab820f129e5588a255d2" 38 | 39 | // Make sure it works when data is passed in. 40 | _, _, err := parseTLSAndMacaroon( 41 | "", "", "mainnet", MacFilename(""), TLSData(tlsData), 42 | MacaroonData(macData), 43 | ) 44 | require.NoError(t, err) 45 | 46 | // Now let's write the data to a file to make sure parseTLSAndMacaroon 47 | // parses that properly as well. 48 | tempDirPath := t.TempDir() 49 | 50 | certPath := tempDirPath + "/tls.cert" 51 | tlsPEMBytes := []byte(tlsData) 52 | 53 | err = os.WriteFile(certPath, tlsPEMBytes, 0644) 54 | require.NoError(t, err) 55 | 56 | macPath := tempDirPath + "/test.macaroon" 57 | macBytes, err := hex.DecodeString(macData) 58 | require.NoError(t, err) 59 | 60 | err = os.WriteFile(macPath, macBytes, 0644) 61 | require.NoError(t, err) 62 | 63 | _, _, err = parseTLSAndMacaroon( 64 | certPath, macPath, "mainnet", MacFilename(""), 65 | ) 66 | require.NoError(t, err) 67 | } 68 | -------------------------------------------------------------------------------- /chainkit_client.go: -------------------------------------------------------------------------------- 1 | package lndclient 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "sync" 7 | "time" 8 | 9 | "github.com/btcsuite/btcd/chaincfg/chainhash" 10 | "github.com/btcsuite/btcd/wire" 11 | "github.com/lightningnetwork/lnd/lnrpc/chainrpc" 12 | "google.golang.org/grpc" 13 | ) 14 | 15 | // ChainKitClient exposes chain functionality. 16 | type ChainKitClient interface { 17 | ServiceClient[chainrpc.ChainKitClient] 18 | 19 | // GetBlock returns a block given the corresponding block hash. 20 | GetBlock(ctx context.Context, hash chainhash.Hash) (*wire.MsgBlock, 21 | error) 22 | 23 | // GetBlockHeader returns a block header given the corresponding block 24 | // hash. 25 | GetBlockHeader(ctx context.Context, 26 | hash chainhash.Hash) (*wire.BlockHeader, error) 27 | 28 | // GetBestBlock returns the latest block hash and current height of the 29 | // valid most-work chain. 30 | GetBestBlock(ctx context.Context) (chainhash.Hash, int32, error) 31 | 32 | // GetBlockHash returns the hash of the block in the best blockchain 33 | // at the given height. 34 | GetBlockHash(ctx context.Context, blockHeight int64) (chainhash.Hash, 35 | error) 36 | } 37 | 38 | type chainKitClient struct { 39 | client chainrpc.ChainKitClient 40 | chainMac serializedMacaroon 41 | timeout time.Duration 42 | 43 | wg sync.WaitGroup 44 | } 45 | 46 | // A compile time check to ensure that chainKitClient implements the 47 | // ChainKitClient interface. 48 | var _ ChainKitClient = (*chainKitClient)(nil) 49 | 50 | func newChainKitClient(conn grpc.ClientConnInterface, 51 | chainMac serializedMacaroon, timeout time.Duration) *chainKitClient { 52 | 53 | return &chainKitClient{ 54 | client: chainrpc.NewChainKitClient(conn), 55 | chainMac: chainMac, 56 | timeout: timeout, 57 | } 58 | } 59 | 60 | func (s *chainKitClient) WaitForFinished() { 61 | s.wg.Wait() 62 | } 63 | 64 | // RawClientWithMacAuth returns a context with the proper macaroon 65 | // authentication, the default RPC timeout, and the raw client. 66 | func (s *chainKitClient) RawClientWithMacAuth( 67 | parentCtx context.Context) (context.Context, time.Duration, 68 | chainrpc.ChainKitClient) { 69 | 70 | return s.chainMac.WithMacaroonAuth(parentCtx), s.timeout, s.client 71 | } 72 | 73 | // GetBlock returns a block given the corresponding block hash. 74 | func (s *chainKitClient) GetBlock(ctxParent context.Context, 75 | hash chainhash.Hash) (*wire.MsgBlock, error) { 76 | 77 | ctx, cancel := context.WithTimeout(ctxParent, s.timeout) 78 | defer cancel() 79 | 80 | macaroonAuth := s.chainMac.WithMacaroonAuth(ctx) 81 | req := &chainrpc.GetBlockRequest{ 82 | BlockHash: hash[:], 83 | } 84 | resp, err := s.client.GetBlock(macaroonAuth, req) 85 | if err != nil { 86 | return nil, err 87 | } 88 | 89 | // Convert raw block bytes into wire.MsgBlock. 90 | msgBlock := &wire.MsgBlock{} 91 | blockReader := bytes.NewReader(resp.RawBlock) 92 | err = msgBlock.Deserialize(blockReader) 93 | if err != nil { 94 | return nil, err 95 | } 96 | 97 | return msgBlock, nil 98 | } 99 | 100 | // GetBlockHeader returns a block header given the corresponding block hash. 101 | func (s *chainKitClient) GetBlockHeader(ctxParent context.Context, 102 | hash chainhash.Hash) (*wire.BlockHeader, error) { 103 | 104 | ctx, cancel := context.WithTimeout(ctxParent, s.timeout) 105 | defer cancel() 106 | 107 | macaroonAuth := s.chainMac.WithMacaroonAuth(ctx) 108 | req := &chainrpc.GetBlockHeaderRequest{ 109 | BlockHash: hash[:], 110 | } 111 | resp, err := s.client.GetBlockHeader(macaroonAuth, req) 112 | if err != nil { 113 | return nil, err 114 | } 115 | 116 | // Convert raw block header bytes into wire.BlockHeader. 117 | blockHeader := &wire.BlockHeader{} 118 | blockReader := bytes.NewReader(resp.RawBlockHeader) 119 | err = blockHeader.Deserialize(blockReader) 120 | if err != nil { 121 | return nil, err 122 | } 123 | 124 | return blockHeader, nil 125 | } 126 | 127 | // GetBestBlock returns the block hash and current height from the valid 128 | // most-work chain. 129 | func (s *chainKitClient) GetBestBlock(ctxParent context.Context) (chainhash.Hash, 130 | int32, error) { 131 | 132 | ctx, cancel := context.WithTimeout(ctxParent, s.timeout) 133 | defer cancel() 134 | 135 | macaroonAuth := s.chainMac.WithMacaroonAuth(ctx) 136 | resp, err := s.client.GetBestBlock( 137 | macaroonAuth, &chainrpc.GetBestBlockRequest{}, 138 | ) 139 | if err != nil { 140 | return chainhash.Hash{}, 0, err 141 | } 142 | 143 | // Cast gRPC block hash bytes as chain hash type. 144 | var blockHash chainhash.Hash 145 | copy(blockHash[:], resp.BlockHash) 146 | 147 | return blockHash, resp.BlockHeight, nil 148 | } 149 | 150 | // GetBlockHash returns the hash of the block in the best blockchain at the 151 | // given height. 152 | func (s *chainKitClient) GetBlockHash(ctxParent context.Context, 153 | blockHeight int64) (chainhash.Hash, error) { 154 | 155 | ctx, cancel := context.WithTimeout(ctxParent, s.timeout) 156 | defer cancel() 157 | 158 | macaroonAuth := s.chainMac.WithMacaroonAuth(ctx) 159 | req := &chainrpc.GetBlockHashRequest{BlockHeight: blockHeight} 160 | resp, err := s.client.GetBlockHash(macaroonAuth, req) 161 | if err != nil { 162 | return chainhash.Hash{}, err 163 | } 164 | 165 | // Cast gRPC block hash bytes as chain hash type. 166 | var blockHash chainhash.Hash 167 | copy(blockHash[:], resp.BlockHash) 168 | 169 | return blockHash, nil 170 | } 171 | -------------------------------------------------------------------------------- /chainnotifier_client.go: -------------------------------------------------------------------------------- 1 | package lndclient 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | "time" 8 | 9 | "github.com/btcsuite/btcd/chaincfg/chainhash" 10 | "github.com/btcsuite/btcd/wire" 11 | "github.com/lightningnetwork/lnd/chainntnfs" 12 | "github.com/lightningnetwork/lnd/lnrpc/chainrpc" 13 | "google.golang.org/grpc" 14 | ) 15 | 16 | // notifierOptions is a set of functional options that allow callers to further 17 | // modify the type of chain even notifications they receive. 18 | type notifierOptions struct { 19 | // includeBlock if true, then the dispatched confirmation notification 20 | // will include the block that mined the transaction. 21 | includeBlock bool 22 | 23 | // reOrgChan if set, will be sent on if the transaction is re-organized 24 | // out of the chain. This channel being set will also imply that we 25 | // don't cancel the notification listener after having received one 26 | // confirmation event. That means the caller manually needs to cancel 27 | // the passed in context to cancel being notified once the required 28 | // number of confirmations have been reached. 29 | reOrgChan chan struct{} 30 | } 31 | 32 | // defaultNotifierOptions returns the set of default options for the notifier. 33 | func defaultNotifierOptions() *notifierOptions { 34 | return ¬ifierOptions{} 35 | } 36 | 37 | // NotifierOption is a functional option that allows a caller to modify the 38 | // events received from the notifier. 39 | type NotifierOption func(*notifierOptions) 40 | 41 | // WithIncludeBlock is an optional argument that allows the caller to specify 42 | // that the block that mined a transaction should be included in the response. 43 | func WithIncludeBlock() NotifierOption { 44 | return func(o *notifierOptions) { 45 | o.includeBlock = true 46 | } 47 | } 48 | 49 | // WithReOrgChan configures a channel that will be sent on if the transaction is 50 | // re-organized out of the chain. This channel being set will also imply that we 51 | // don't cancel the notification listener after having received one confirmation 52 | // event. That means the caller manually needs to cancel the passed in context 53 | // to cancel being notified once the required number of confirmations have been 54 | // reached. 55 | func WithReOrgChan(reOrgChan chan struct{}) NotifierOption { 56 | return func(o *notifierOptions) { 57 | o.reOrgChan = reOrgChan 58 | } 59 | } 60 | 61 | // ChainNotifierClient exposes base lightning functionality. 62 | type ChainNotifierClient interface { 63 | ServiceClient[chainrpc.ChainNotifierClient] 64 | 65 | RegisterBlockEpochNtfn(ctx context.Context) ( 66 | chan int32, chan error, error) 67 | 68 | RegisterConfirmationsNtfn(ctx context.Context, txid *chainhash.Hash, 69 | pkScript []byte, numConfs, heightHint int32, 70 | opts ...NotifierOption) (chan *chainntnfs.TxConfirmation, 71 | chan error, error) 72 | 73 | RegisterSpendNtfn(ctx context.Context, 74 | outpoint *wire.OutPoint, pkScript []byte, heightHint int32) ( 75 | chan *chainntnfs.SpendDetail, chan error, error) 76 | } 77 | 78 | type chainNotifierClient struct { 79 | client chainrpc.ChainNotifierClient 80 | chainMac serializedMacaroon 81 | timeout time.Duration 82 | 83 | wg sync.WaitGroup 84 | } 85 | 86 | // A compile time check to ensure that chainNotifierClient implements the 87 | // ChainNotifierClient interface. 88 | var _ ChainNotifierClient = (*chainNotifierClient)(nil) 89 | 90 | func newChainNotifierClient(conn grpc.ClientConnInterface, 91 | chainMac serializedMacaroon, timeout time.Duration) *chainNotifierClient { 92 | 93 | return &chainNotifierClient{ 94 | client: chainrpc.NewChainNotifierClient(conn), 95 | chainMac: chainMac, 96 | timeout: timeout, 97 | } 98 | } 99 | 100 | func (s *chainNotifierClient) WaitForFinished() { 101 | s.wg.Wait() 102 | } 103 | 104 | // RawClientWithMacAuth returns a context with the proper macaroon 105 | // authentication, the default RPC timeout, and the raw client. 106 | func (s *chainNotifierClient) RawClientWithMacAuth( 107 | parentCtx context.Context) (context.Context, time.Duration, 108 | chainrpc.ChainNotifierClient) { 109 | 110 | return s.chainMac.WithMacaroonAuth(parentCtx), s.timeout, s.client 111 | } 112 | 113 | func (s *chainNotifierClient) RegisterSpendNtfn(ctx context.Context, 114 | outpoint *wire.OutPoint, pkScript []byte, heightHint int32) ( 115 | chan *chainntnfs.SpendDetail, chan error, error) { 116 | 117 | var rpcOutpoint *chainrpc.Outpoint 118 | if outpoint != nil { 119 | rpcOutpoint = &chainrpc.Outpoint{ 120 | Hash: outpoint.Hash[:], 121 | Index: outpoint.Index, 122 | } 123 | } 124 | 125 | macaroonAuth := s.chainMac.WithMacaroonAuth(ctx) 126 | resp, err := s.client.RegisterSpendNtfn(macaroonAuth, &chainrpc.SpendRequest{ 127 | HeightHint: uint32(heightHint), 128 | Outpoint: rpcOutpoint, 129 | Script: pkScript, 130 | }) 131 | if err != nil { 132 | return nil, nil, err 133 | } 134 | 135 | spendChan := make(chan *chainntnfs.SpendDetail, 1) 136 | errChan := make(chan error, 1) 137 | 138 | processSpendDetail := func(d *chainrpc.SpendDetails) error { 139 | outpointHash, err := chainhash.NewHash(d.SpendingOutpoint.Hash) 140 | if err != nil { 141 | return err 142 | } 143 | txHash, err := chainhash.NewHash(d.SpendingTxHash) 144 | if err != nil { 145 | return err 146 | } 147 | tx, err := decodeTx(d.RawSpendingTx) 148 | if err != nil { 149 | return err 150 | } 151 | spendChan <- &chainntnfs.SpendDetail{ 152 | SpentOutPoint: &wire.OutPoint{ 153 | Hash: *outpointHash, 154 | Index: d.SpendingOutpoint.Index, 155 | }, 156 | SpenderTxHash: txHash, 157 | SpenderInputIndex: d.SpendingInputIndex, 158 | SpendingTx: tx, 159 | SpendingHeight: int32(d.SpendingHeight), 160 | } 161 | 162 | return nil 163 | } 164 | 165 | s.wg.Add(1) 166 | go func() { 167 | defer s.wg.Done() 168 | for { 169 | spendEvent, err := resp.Recv() 170 | if err != nil { 171 | errChan <- err 172 | return 173 | } 174 | 175 | c, ok := spendEvent.Event.(*chainrpc.SpendEvent_Spend) 176 | if ok { 177 | err := processSpendDetail(c.Spend) 178 | if err != nil { 179 | errChan <- err 180 | } 181 | return 182 | } 183 | } 184 | }() 185 | 186 | return spendChan, errChan, nil 187 | } 188 | 189 | func (s *chainNotifierClient) RegisterConfirmationsNtfn(ctx context.Context, 190 | txid *chainhash.Hash, pkScript []byte, numConfs, heightHint int32, 191 | optFuncs ...NotifierOption) (chan *chainntnfs.TxConfirmation, 192 | chan error, error) { 193 | 194 | opts := defaultNotifierOptions() 195 | for _, optFunc := range optFuncs { 196 | optFunc(opts) 197 | } 198 | 199 | var txidSlice []byte 200 | if txid != nil { 201 | txidSlice = txid[:] 202 | } 203 | confStream, err := s.client.RegisterConfirmationsNtfn( 204 | s.chainMac.WithMacaroonAuth(ctx), &chainrpc.ConfRequest{ 205 | Script: pkScript, 206 | NumConfs: uint32(numConfs), 207 | HeightHint: uint32(heightHint), 208 | Txid: txidSlice, 209 | IncludeBlock: opts.includeBlock, 210 | }, 211 | ) 212 | if err != nil { 213 | return nil, nil, err 214 | } 215 | 216 | confChan := make(chan *chainntnfs.TxConfirmation, 1) 217 | errChan := make(chan error, 1) 218 | 219 | s.wg.Add(1) 220 | go func() { 221 | defer s.wg.Done() 222 | 223 | for { 224 | var confEvent *chainrpc.ConfEvent 225 | confEvent, err := confStream.Recv() 226 | if err != nil { 227 | errChan <- err 228 | return 229 | } 230 | 231 | switch c := confEvent.Event.(type) { 232 | // Script confirmed. 233 | case *chainrpc.ConfEvent_Conf: 234 | tx, err := decodeTx(c.Conf.RawTx) 235 | if err != nil { 236 | errChan <- err 237 | return 238 | } 239 | 240 | var block *wire.MsgBlock 241 | if opts.includeBlock { 242 | block, err = decodeBlock( 243 | c.Conf.RawBlock, 244 | ) 245 | if err != nil { 246 | errChan <- err 247 | return 248 | } 249 | } 250 | 251 | blockHash, err := chainhash.NewHash( 252 | c.Conf.BlockHash, 253 | ) 254 | if err != nil { 255 | errChan <- err 256 | return 257 | } 258 | 259 | confChan <- &chainntnfs.TxConfirmation{ 260 | BlockHeight: c.Conf.BlockHeight, 261 | BlockHash: blockHash, 262 | Tx: tx, 263 | TxIndex: c.Conf.TxIndex, 264 | Block: block, 265 | } 266 | 267 | // If we're running in re-org aware mode, then 268 | // we don't return here, since we might want to 269 | // be informed about the new block we got 270 | // confirmed in after a re-org. 271 | if opts.reOrgChan == nil { 272 | return 273 | } 274 | 275 | // On a re-org, we just need to signal, we don't have 276 | // any additional information. But we only signal if the 277 | // caller requested to be notified about re-orgs. 278 | case *chainrpc.ConfEvent_Reorg: 279 | if opts.reOrgChan != nil { 280 | select { 281 | case opts.reOrgChan <- struct{}{}: 282 | case <-ctx.Done(): 283 | return 284 | } 285 | } 286 | continue 287 | 288 | // Nil event, should never happen. 289 | case nil: 290 | errChan <- fmt.Errorf("conf event empty") 291 | return 292 | 293 | // Unexpected type. 294 | default: 295 | errChan <- fmt.Errorf("conf event has " + 296 | "unexpected type") 297 | return 298 | } 299 | } 300 | }() 301 | 302 | return confChan, errChan, nil 303 | } 304 | 305 | func (s *chainNotifierClient) RegisterBlockEpochNtfn(ctx context.Context) ( 306 | chan int32, chan error, error) { 307 | 308 | blockEpochClient, err := s.client.RegisterBlockEpochNtfn( 309 | s.chainMac.WithMacaroonAuth(ctx), &chainrpc.BlockEpoch{}, 310 | ) 311 | if err != nil { 312 | return nil, nil, err 313 | } 314 | 315 | blockErrorChan := make(chan error, 1) 316 | blockEpochChan := make(chan int32) 317 | 318 | // Start block epoch goroutine. 319 | s.wg.Add(1) 320 | go func() { 321 | defer s.wg.Done() 322 | for { 323 | epoch, err := blockEpochClient.Recv() 324 | if err != nil { 325 | blockErrorChan <- err 326 | return 327 | } 328 | 329 | select { 330 | case blockEpochChan <- int32(epoch.Height): 331 | case <-ctx.Done(): 332 | return 333 | } 334 | } 335 | }() 336 | 337 | return blockEpochChan, blockErrorChan, nil 338 | } 339 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lightninglabs/lndclient 2 | 3 | require ( 4 | github.com/btcsuite/btcd v0.24.3-0.20250318170759-4f4ea81776d6 5 | github.com/btcsuite/btcd/btcec/v2 v2.3.4 6 | github.com/btcsuite/btcd/btcutil v1.1.5 7 | github.com/btcsuite/btcd/btcutil/psbt v1.1.8 8 | github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 9 | github.com/btcsuite/btclog/v2 v2.0.1-0.20250110154127-3ae4bf1cb318 10 | github.com/btcsuite/btcwallet v0.16.13 11 | github.com/btcsuite/btcwallet/wtxmgr v1.5.6 12 | github.com/lightningnetwork/lnd v0.19.0-beta 13 | github.com/lightningnetwork/lnd/kvdb v1.4.16 14 | github.com/stretchr/testify v1.10.0 15 | google.golang.org/grpc v1.59.0 16 | gopkg.in/macaroon-bakery.v2 v2.0.1 17 | gopkg.in/macaroon.v2 v2.1.0 18 | ) 19 | 20 | require ( 21 | dario.cat/mergo v1.0.1 // indirect 22 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect 23 | github.com/Microsoft/go-winio v0.6.1 // indirect 24 | github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect 25 | github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect 26 | github.com/aead/siphash v1.0.1 // indirect 27 | github.com/beorn7/perks v1.0.1 // indirect 28 | github.com/btcsuite/btclog v0.0.0-20241003133417-09c4e92e319c // indirect 29 | github.com/btcsuite/btcwallet/wallet/txauthor v1.3.5 // indirect 30 | github.com/btcsuite/btcwallet/wallet/txrules v1.2.2 // indirect 31 | github.com/btcsuite/btcwallet/wallet/txsizes v1.2.5 // indirect 32 | github.com/btcsuite/btcwallet/walletdb v1.5.1 // indirect 33 | github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect 34 | github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect 35 | github.com/btcsuite/winsvc v1.0.0 // indirect 36 | github.com/cenkalti/backoff/v4 v4.2.1 // indirect 37 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 38 | github.com/containerd/continuity v0.3.0 // indirect 39 | github.com/coreos/go-semver v0.3.0 // indirect 40 | github.com/coreos/go-systemd/v22 v22.5.0 // indirect 41 | github.com/davecgh/go-spew v1.1.1 // indirect 42 | github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect 43 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect 44 | github.com/decred/dcrd/lru v1.1.2 // indirect 45 | github.com/distribution/reference v0.6.0 // indirect 46 | github.com/docker/cli v28.0.1+incompatible // indirect 47 | github.com/docker/docker v28.0.1+incompatible // indirect 48 | github.com/docker/go-connections v0.4.0 // indirect 49 | github.com/docker/go-units v0.5.0 // indirect 50 | github.com/dustin/go-humanize v1.0.1 // indirect 51 | github.com/fergusstrange/embedded-postgres v1.25.0 // indirect 52 | github.com/go-errors/errors v1.0.1 // indirect 53 | github.com/go-logr/logr v1.4.2 // indirect 54 | github.com/go-logr/stdr v1.2.2 // indirect 55 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect 56 | github.com/gofrs/uuid v4.2.0+incompatible // indirect 57 | github.com/gogo/protobuf v1.3.2 // indirect 58 | github.com/golang-jwt/jwt/v4 v4.5.2 // indirect 59 | github.com/golang-migrate/migrate/v4 v4.17.0 // indirect 60 | github.com/golang/protobuf v1.5.3 // indirect 61 | github.com/golang/snappy v0.0.4 // indirect 62 | github.com/google/btree v1.0.1 // indirect 63 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect 64 | github.com/google/uuid v1.6.0 // indirect 65 | github.com/gorilla/websocket v1.5.0 // indirect 66 | github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect 67 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect 68 | github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect 69 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect 70 | github.com/hashicorp/errwrap v1.1.0 // indirect 71 | github.com/hashicorp/go-multierror v1.1.1 // indirect 72 | github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect 73 | github.com/jackc/chunkreader/v2 v2.0.1 // indirect 74 | github.com/jackc/pgconn v1.14.3 // indirect 75 | github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 // indirect 76 | github.com/jackc/pgio v1.0.0 // indirect 77 | github.com/jackc/pgpassfile v1.0.0 // indirect 78 | github.com/jackc/pgproto3/v2 v2.3.3 // indirect 79 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect 80 | github.com/jackc/pgtype v1.14.0 // indirect 81 | github.com/jackc/pgx/v4 v4.18.2 // indirect 82 | github.com/jackc/pgx/v5 v5.5.4 // indirect 83 | github.com/jackc/puddle/v2 v2.2.1 // indirect 84 | github.com/jessevdk/go-flags v1.4.0 // indirect 85 | github.com/jonboulle/clockwork v0.2.2 // indirect 86 | github.com/jrick/logrotate v1.1.2 // indirect 87 | github.com/json-iterator/go v1.1.11 // indirect 88 | github.com/juju/clock v0.0.0-20220203021603-d9deb868a28a // indirect 89 | github.com/juju/collections v0.0.0-20220203020748-febd7cad8a7a // indirect 90 | github.com/juju/errors v0.0.0-20220331221717-b38fca44723b // indirect 91 | github.com/juju/loggo v0.0.0-20210728185423-eebad3a902c4 // indirect 92 | github.com/juju/mgo/v2 v2.0.0-20220111072304-f200228f1090 // indirect 93 | github.com/juju/retry v0.0.0-20220204093819-62423bf33287 // indirect 94 | github.com/juju/utils/v3 v3.0.0-20220203023959-c3fbc78a33b0 // indirect 95 | github.com/juju/version/v2 v2.0.0-20220204124744-fc9915e3d935 // indirect 96 | github.com/kkdai/bstream v1.0.0 // indirect 97 | github.com/klauspost/compress v1.17.9 // indirect 98 | github.com/lib/pq v1.10.9 // indirect 99 | github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf // indirect 100 | github.com/lightninglabs/neutrino v0.16.1 // indirect 101 | github.com/lightninglabs/neutrino/cache v1.1.2 // indirect 102 | github.com/lightningnetwork/lightning-onion v1.2.1-0.20240712235311-98bd56499dfb // indirect 103 | github.com/lightningnetwork/lnd/clock v1.1.1 // indirect 104 | github.com/lightningnetwork/lnd/fn/v2 v2.0.8 // indirect 105 | github.com/lightningnetwork/lnd/healthcheck v1.2.6 // indirect 106 | github.com/lightningnetwork/lnd/queue v1.1.1 // indirect 107 | github.com/lightningnetwork/lnd/sqldb v1.0.9 // indirect 108 | github.com/lightningnetwork/lnd/ticker v1.1.1 // indirect 109 | github.com/lightningnetwork/lnd/tlv v1.3.1 // indirect 110 | github.com/lightningnetwork/lnd/tor v1.1.6 // indirect 111 | github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796 // indirect 112 | github.com/mattn/go-isatty v0.0.20 // indirect 113 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect 114 | github.com/miekg/dns v1.1.43 // indirect 115 | github.com/moby/docker-image-spec v1.3.1 // indirect 116 | github.com/moby/sys/user v0.3.0 // indirect 117 | github.com/moby/term v0.5.0 // indirect 118 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 119 | github.com/modern-go/reflect2 v1.0.1 // indirect 120 | github.com/ncruces/go-strftime v0.1.9 // indirect 121 | github.com/opencontainers/go-digest v1.0.0 // indirect 122 | github.com/opencontainers/image-spec v1.0.2 // indirect 123 | github.com/opencontainers/runc v1.2.0 // indirect 124 | github.com/ory/dockertest/v3 v3.10.0 // indirect 125 | github.com/pkg/errors v0.9.1 // indirect 126 | github.com/pmezard/go-difflib v1.0.0 // indirect 127 | github.com/prometheus/client_golang v1.11.1 // indirect 128 | github.com/prometheus/client_model v0.2.0 // indirect 129 | github.com/prometheus/common v0.26.0 // indirect 130 | github.com/prometheus/procfs v0.6.0 // indirect 131 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 132 | github.com/rogpeppe/fastuuid v1.2.0 // indirect 133 | github.com/sirupsen/logrus v1.9.3 // indirect 134 | github.com/soheilhy/cmux v0.1.5 // indirect 135 | github.com/spf13/pflag v1.0.5 // indirect 136 | github.com/stretchr/objx v0.5.2 // indirect 137 | github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect 138 | github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect 139 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect 140 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect 141 | github.com/xeipuuv/gojsonschema v1.2.0 // indirect 142 | github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect 143 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect 144 | go.etcd.io/bbolt v1.3.11 // indirect 145 | go.etcd.io/etcd/api/v3 v3.5.12 // indirect 146 | go.etcd.io/etcd/client/pkg/v3 v3.5.12 // indirect 147 | go.etcd.io/etcd/client/v2 v2.305.12 // indirect 148 | go.etcd.io/etcd/client/v3 v3.5.12 // indirect 149 | go.etcd.io/etcd/pkg/v3 v3.5.12 // indirect 150 | go.etcd.io/etcd/raft/v3 v3.5.12 // indirect 151 | go.etcd.io/etcd/server/v3 v3.5.12 // indirect 152 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 153 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.0 // indirect 154 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect 155 | go.opentelemetry.io/otel v1.35.0 // indirect 156 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0 // indirect 157 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.20.0 // indirect 158 | go.opentelemetry.io/otel/metric v1.35.0 // indirect 159 | go.opentelemetry.io/otel/sdk v1.35.0 // indirect 160 | go.opentelemetry.io/otel/trace v1.35.0 // indirect 161 | go.opentelemetry.io/proto/otlp v1.0.0 // indirect 162 | go.uber.org/atomic v1.7.0 // indirect 163 | go.uber.org/multierr v1.6.0 // indirect 164 | go.uber.org/zap v1.17.0 // indirect 165 | golang.org/x/crypto v0.36.0 // indirect 166 | golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect 167 | golang.org/x/mod v0.17.0 // indirect 168 | golang.org/x/net v0.38.0 // indirect 169 | golang.org/x/sync v0.12.0 // indirect 170 | golang.org/x/sys v0.31.0 // indirect 171 | golang.org/x/term v0.30.0 // indirect 172 | golang.org/x/text v0.23.0 // indirect 173 | golang.org/x/time v0.3.0 // indirect 174 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect 175 | google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b // indirect 176 | google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b // indirect 177 | google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 // indirect 178 | google.golang.org/protobuf v1.33.0 // indirect 179 | gopkg.in/errgo.v1 v1.0.1 // indirect 180 | gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect 181 | gopkg.in/yaml.v2 v2.4.0 // indirect 182 | gopkg.in/yaml.v3 v3.0.1 // indirect 183 | modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect 184 | modernc.org/libc v1.49.3 // indirect 185 | modernc.org/mathutil v1.6.0 // indirect 186 | modernc.org/memory v1.8.0 // indirect 187 | modernc.org/sqlite v1.29.10 // indirect 188 | modernc.org/strutil v1.2.0 // indirect 189 | modernc.org/token v1.1.0 // indirect 190 | pgregory.net/rapid v1.2.0 // indirect 191 | sigs.k8s.io/yaml v1.2.0 // indirect 192 | ) 193 | 194 | // We want to format raw bytes as hex instead of base64. The forked version 195 | // allows us to specify that as an option. 196 | replace google.golang.org/protobuf => github.com/lightninglabs/protobuf-go-hex-display v1.33.0-hex-display 197 | 198 | go 1.23.6 199 | -------------------------------------------------------------------------------- /invoices_client.go: -------------------------------------------------------------------------------- 1 | package lndclient 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "sync" 9 | "time" 10 | 11 | "github.com/btcsuite/btcd/btcutil" 12 | invpkg "github.com/lightningnetwork/lnd/invoices" 13 | "github.com/lightningnetwork/lnd/lnrpc" 14 | "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" 15 | "github.com/lightningnetwork/lnd/lntypes" 16 | "github.com/lightningnetwork/lnd/lnwire" 17 | "google.golang.org/grpc" 18 | ) 19 | 20 | // InvoiceHtlcModifyRequest is a request to modify an HTLC that is attempting to 21 | // settle an invoice. 22 | type InvoiceHtlcModifyRequest struct { 23 | // Invoice is the current state of the invoice, _before_ this HTLC is 24 | // applied. Any HTLC in the invoice is a previously accepted/settled 25 | // one. 26 | Invoice *lnrpc.Invoice 27 | 28 | // CircuitKey is the circuit key of the HTLC that is attempting to 29 | // settle the invoice. 30 | CircuitKey invpkg.CircuitKey 31 | 32 | // ExitHtlcAmt is the amount of the HTLC that is attempting to settle 33 | // the invoice. 34 | ExitHtlcAmt lnwire.MilliSatoshi 35 | 36 | // ExitHtlcExpiry is the expiry of the HTLC that is attempting to settle 37 | // the invoice. 38 | ExitHtlcExpiry uint32 39 | 40 | // CurrentHeight is the current block height. 41 | CurrentHeight uint32 42 | 43 | // WireCustomRecords is the wire custom records of the HTLC that is 44 | // attempting to settle the invoice. 45 | WireCustomRecords lnwire.CustomRecords 46 | } 47 | 48 | // InvoiceHtlcModifyResponse is a response to an HTLC modification request. 49 | type InvoiceHtlcModifyResponse struct { 50 | // CircuitKey is the circuit key the response is for. 51 | CircuitKey invpkg.CircuitKey 52 | 53 | // AmtPaid is the amount the HTLC contributes toward settling the 54 | // invoice. This amount can be different from the on-chain amount of the 55 | // HTLC in case of custom channels. To not modify the amount and use the 56 | // on-chain amount, set this to 0. 57 | AmtPaid lnwire.MilliSatoshi 58 | 59 | // CancelSet is a flag that indicates whether the HTLCs associated with 60 | // the invoice should get cancelled. 61 | CancelSet bool 62 | } 63 | 64 | // InvoiceHtlcModifyHandler is a function that handles an HTLC modification 65 | // request. 66 | type InvoiceHtlcModifyHandler func(context.Context, 67 | InvoiceHtlcModifyRequest) (*InvoiceHtlcModifyResponse, error) 68 | 69 | // InvoicesClient exposes invoice functionality. 70 | type InvoicesClient interface { 71 | ServiceClient[invoicesrpc.InvoicesClient] 72 | 73 | SubscribeSingleInvoice(ctx context.Context, hash lntypes.Hash) ( 74 | <-chan InvoiceUpdate, <-chan error, error) 75 | 76 | SettleInvoice(ctx context.Context, preimage lntypes.Preimage) error 77 | 78 | CancelInvoice(ctx context.Context, hash lntypes.Hash) error 79 | 80 | AddHoldInvoice(ctx context.Context, in *invoicesrpc.AddInvoiceData) ( 81 | string, error) 82 | 83 | // HtlcModifier is a bidirectional streaming RPC that allows a client to 84 | // intercept and modify the HTLCs that attempt to settle the given 85 | // invoice. The server will send HTLCs of invoices to the client and the 86 | // client can modify some aspects of the HTLC in order to pass the 87 | // invoice acceptance tests. 88 | HtlcModifier(ctx context.Context, 89 | handler InvoiceHtlcModifyHandler) error 90 | } 91 | 92 | // InvoiceUpdate contains a state update for an invoice. 93 | type InvoiceUpdate struct { 94 | State invpkg.ContractState 95 | AmtPaid btcutil.Amount 96 | } 97 | 98 | type invoicesClient struct { 99 | client invoicesrpc.InvoicesClient 100 | invoiceMac serializedMacaroon 101 | timeout time.Duration 102 | quitOnce sync.Once 103 | quit chan struct{} 104 | wg sync.WaitGroup 105 | } 106 | 107 | // A compile time check to ensure that invoicesClient implements the 108 | // InvoicesClient interface. 109 | var _ InvoicesClient = (*invoicesClient)(nil) 110 | 111 | func newInvoicesClient(conn grpc.ClientConnInterface, 112 | invoiceMac serializedMacaroon, timeout time.Duration) *invoicesClient { 113 | 114 | return &invoicesClient{ 115 | client: invoicesrpc.NewInvoicesClient(conn), 116 | invoiceMac: invoiceMac, 117 | timeout: timeout, 118 | quit: make(chan struct{}), 119 | } 120 | } 121 | 122 | func (s *invoicesClient) WaitForFinished() { 123 | s.quitOnce.Do(func() { 124 | close(s.quit) 125 | }) 126 | 127 | s.wg.Wait() 128 | } 129 | 130 | // RawClientWithMacAuth returns a context with the proper macaroon 131 | // authentication, the default RPC timeout, and the raw client. 132 | func (s *invoicesClient) RawClientWithMacAuth( 133 | parentCtx context.Context) (context.Context, time.Duration, 134 | invoicesrpc.InvoicesClient) { 135 | 136 | return s.invoiceMac.WithMacaroonAuth(parentCtx), s.timeout, s.client 137 | } 138 | 139 | func (s *invoicesClient) SettleInvoice(ctx context.Context, 140 | preimage lntypes.Preimage) error { 141 | 142 | timeoutCtx, cancel := context.WithTimeout(ctx, s.timeout) 143 | defer cancel() 144 | 145 | rpcCtx := s.invoiceMac.WithMacaroonAuth(timeoutCtx) 146 | _, err := s.client.SettleInvoice(rpcCtx, &invoicesrpc.SettleInvoiceMsg{ 147 | Preimage: preimage[:], 148 | }) 149 | 150 | return err 151 | } 152 | 153 | func (s *invoicesClient) CancelInvoice(ctx context.Context, 154 | hash lntypes.Hash) error { 155 | 156 | rpcCtx, cancel := context.WithTimeout(ctx, s.timeout) 157 | defer cancel() 158 | 159 | rpcCtx = s.invoiceMac.WithMacaroonAuth(rpcCtx) 160 | _, err := s.client.CancelInvoice(rpcCtx, &invoicesrpc.CancelInvoiceMsg{ 161 | PaymentHash: hash[:], 162 | }) 163 | 164 | return err 165 | } 166 | 167 | func (s *invoicesClient) SubscribeSingleInvoice(ctx context.Context, 168 | hash lntypes.Hash) (<-chan InvoiceUpdate, 169 | <-chan error, error) { 170 | 171 | invoiceStream, err := s.client.SubscribeSingleInvoice( 172 | s.invoiceMac.WithMacaroonAuth(ctx), 173 | &invoicesrpc.SubscribeSingleInvoiceRequest{ 174 | RHash: hash[:], 175 | }, 176 | ) 177 | if err != nil { 178 | return nil, nil, err 179 | } 180 | 181 | updateChan := make(chan InvoiceUpdate) 182 | errChan := make(chan error, 1) 183 | 184 | // Invoice updates goroutine. 185 | s.wg.Add(1) 186 | go func() { 187 | defer s.wg.Done() 188 | for { 189 | invoice, err := invoiceStream.Recv() 190 | if err != nil { 191 | // If we get an EOF error, the invoice has 192 | // reached a final state and the server is 193 | // finished sending us updates. We close both 194 | // channels to signal that we are done sending 195 | // values on them and return. 196 | if err == io.EOF { 197 | close(updateChan) 198 | close(errChan) 199 | return 200 | } 201 | 202 | errChan <- err 203 | return 204 | } 205 | 206 | state, err := fromRPCInvoiceState(invoice.State) 207 | if err != nil { 208 | errChan <- err 209 | return 210 | } 211 | 212 | select { 213 | case updateChan <- InvoiceUpdate{ 214 | State: state, 215 | AmtPaid: btcutil.Amount(invoice.AmtPaidSat), 216 | }: 217 | case <-ctx.Done(): 218 | return 219 | } 220 | } 221 | }() 222 | 223 | return updateChan, errChan, nil 224 | } 225 | 226 | func (s *invoicesClient) AddHoldInvoice(ctx context.Context, 227 | in *invoicesrpc.AddInvoiceData) (string, error) { 228 | 229 | rpcCtx, cancel := context.WithTimeout(ctx, s.timeout) 230 | defer cancel() 231 | 232 | routeHints, err := marshallRouteHints(in.RouteHints) 233 | if err != nil { 234 | return "", fmt.Errorf("failed to marshal route hints: %v", err) 235 | } 236 | 237 | rpcIn := &invoicesrpc.AddHoldInvoiceRequest{ 238 | Memo: in.Memo, 239 | Hash: in.Hash[:], 240 | ValueMsat: int64(in.Value), 241 | Expiry: in.Expiry, 242 | CltvExpiry: in.CltvExpiry, 243 | Private: in.Private, 244 | RouteHints: routeHints, 245 | DescriptionHash: in.DescriptionHash, 246 | FallbackAddr: in.FallbackAddr, 247 | } 248 | 249 | rpcCtx = s.invoiceMac.WithMacaroonAuth(rpcCtx) 250 | resp, err := s.client.AddHoldInvoice(rpcCtx, rpcIn) 251 | if err != nil { 252 | return "", err 253 | } 254 | return resp.PaymentRequest, nil 255 | } 256 | 257 | func fromRPCInvoiceState(state lnrpc.Invoice_InvoiceState) ( 258 | invpkg.ContractState, error) { 259 | 260 | switch state { 261 | case lnrpc.Invoice_OPEN: 262 | return invpkg.ContractOpen, nil 263 | 264 | case lnrpc.Invoice_ACCEPTED: 265 | return invpkg.ContractAccepted, nil 266 | 267 | case lnrpc.Invoice_SETTLED: 268 | return invpkg.ContractSettled, nil 269 | 270 | case lnrpc.Invoice_CANCELED: 271 | return invpkg.ContractCanceled, nil 272 | } 273 | 274 | return 0, errors.New("unknown state") 275 | } 276 | 277 | // HtlcModifier is a bidirectional streaming RPC that allows a client to 278 | // intercept and modify the HTLCs that attempt to settle the given invoice. The 279 | // server will send HTLCs of invoices to the client and the client can modify 280 | // some aspects of the HTLC in order to pass the invoice acceptance tests. 281 | func (s *invoicesClient) HtlcModifier(ctx context.Context, 282 | handler InvoiceHtlcModifyHandler) error { 283 | 284 | // Create a child context that will be canceled when this function 285 | // exits. We use this context to be able to cancel goroutines when we 286 | // exit on errors, because the parent context won't be canceled in that 287 | // case. 288 | ctx, cancel := context.WithCancel(ctx) 289 | defer cancel() 290 | 291 | stream, err := s.client.HtlcModifier( 292 | s.invoiceMac.WithMacaroonAuth(ctx), 293 | ) 294 | if err != nil { 295 | return err 296 | } 297 | 298 | // Create an error channel that we'll send errors on if any of our 299 | // goroutines fail. We buffer by 1 so that the goroutine doesn't depend 300 | // on the stream being read, and select on context cancellation and 301 | // quit channel so that we do not block in the case where we exit with 302 | // multiple errors. 303 | errChan := make(chan error, 1) 304 | 305 | sendErr := func(err error) { 306 | select { 307 | case errChan <- err: 308 | case <-ctx.Done(): 309 | case <-s.quit: 310 | } 311 | } 312 | 313 | // Start a goroutine that consumes interception requests from lnd and 314 | // sends them into our requests channel for handling. The requests 315 | // channel is not buffered because we expect all requests to be handled 316 | // until this function exits, at which point we expect our context to 317 | // be canceled or quit channel to be closed. 318 | requestChan := make(chan InvoiceHtlcModifyRequest) 319 | s.wg.Add(1) 320 | go func() { 321 | defer s.wg.Done() 322 | 323 | for { 324 | // Do a quick check whether our client context has been 325 | // canceled so that we can exit sooner if needed. 326 | if ctx.Err() != nil { 327 | return 328 | } 329 | 330 | req, err := stream.Recv() 331 | if err != nil { 332 | sendErr(err) 333 | return 334 | } 335 | 336 | wireCustomRecords := req.ExitHtlcWireCustomRecords 337 | interceptReq := InvoiceHtlcModifyRequest{ 338 | Invoice: req.Invoice, 339 | CircuitKey: invpkg.CircuitKey{ 340 | ChanID: lnwire.NewShortChanIDFromInt( 341 | req.ExitHtlcCircuitKey.ChanId, 342 | ), 343 | HtlcID: req.ExitHtlcCircuitKey.HtlcId, 344 | }, 345 | ExitHtlcAmt: lnwire.MilliSatoshi( 346 | req.ExitHtlcAmt, 347 | ), 348 | ExitHtlcExpiry: req.ExitHtlcExpiry, 349 | CurrentHeight: req.CurrentHeight, 350 | WireCustomRecords: wireCustomRecords, 351 | } 352 | 353 | // Try to send our interception request, failing on 354 | // context cancel or router exit. 355 | select { 356 | case requestChan <- interceptReq: 357 | 358 | case <-s.quit: 359 | sendErr(ErrRouterShuttingDown) 360 | return 361 | 362 | case <-ctx.Done(): 363 | sendErr(ctx.Err()) 364 | return 365 | } 366 | } 367 | }() 368 | 369 | for { 370 | select { 371 | case request := <-requestChan: 372 | // Handle requests in a goroutine so that the handler 373 | // provided to this function can be blocking. If we 374 | // get an error, send it into our error channel to 375 | // shut down the interceptor. 376 | s.wg.Add(1) 377 | go func() { 378 | defer s.wg.Done() 379 | 380 | // Get a response from handler, this may block 381 | // for a while. 382 | resp, err := handler(ctx, request) 383 | if err != nil { 384 | sendErr(err) 385 | return 386 | } 387 | 388 | key := resp.CircuitKey 389 | amtPaid := uint64(resp.AmtPaid) 390 | rpcResp := &invoicesrpc.HtlcModifyResponse{ 391 | CircuitKey: &invoicesrpc.CircuitKey{ 392 | ChanId: key.ChanID.ToUint64(), 393 | HtlcId: key.HtlcID, 394 | }, 395 | AmtPaid: &amtPaid, 396 | CancelSet: resp.CancelSet, 397 | } 398 | 399 | if err := stream.Send(rpcResp); err != nil { 400 | sendErr(err) 401 | return 402 | } 403 | }() 404 | 405 | // If one of our goroutines fails, exit with the error that 406 | // occurred. 407 | case err := <-errChan: 408 | return err 409 | 410 | case <-s.quit: 411 | return ErrRouterShuttingDown 412 | 413 | case <-ctx.Done(): 414 | return ctx.Err() 415 | } 416 | } 417 | } 418 | -------------------------------------------------------------------------------- /lightning_client_test.go: -------------------------------------------------------------------------------- 1 | package lndclient 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | 8 | "github.com/lightningnetwork/lnd/lnrpc" 9 | "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" 10 | "github.com/lightningnetwork/lnd/lntypes" 11 | "github.com/lightningnetwork/lnd/lnwire" 12 | "github.com/stretchr/testify/require" 13 | "google.golang.org/grpc" 14 | ) 15 | 16 | // addInvoiceArg records the args used in a call to mockRPCClient.AddInvoice. 17 | type addInvoiceArg struct { 18 | in *lnrpc.Invoice 19 | opts []grpc.CallOption 20 | } 21 | 22 | // mockRPCClient implements lnrpc.LightningClient with dynamic method 23 | // implementations and call spying. 24 | type mockRPCClient struct { 25 | lnrpc.LightningClient 26 | 27 | addInvoice func(in *lnrpc.Invoice, opts ...grpc.CallOption) ( 28 | *lnrpc.AddInvoiceResponse, error) 29 | addInvoiceArgs []addInvoiceArg 30 | } 31 | 32 | func (m *mockRPCClient) AddInvoice(ctx context.Context, in *lnrpc.Invoice, 33 | opts ...grpc.CallOption) (*lnrpc.AddInvoiceResponse, error) { 34 | 35 | m.addInvoiceArgs = append(m.addInvoiceArgs, addInvoiceArg{ 36 | in: in, 37 | opts: opts, 38 | }) 39 | 40 | return m.addInvoice(in, opts...) 41 | } 42 | 43 | // TestLightningClientAddInvoice ensures that adding an invoice via 44 | // lightningClient is completed as expected. 45 | func TestLightningClientAddInvoice(t *testing.T) { 46 | // Define constants / fixtures. 47 | var validPreimage lntypes.Preimage 48 | copy(validPreimage[:], "valid preimage") 49 | var validRHash lntypes.Hash 50 | copy(validRHash[:], "valid hash") 51 | validAddInvoiceData := &invoicesrpc.AddInvoiceData{ 52 | Memo: "fake memo", 53 | Preimage: &validPreimage, 54 | Hash: &validRHash, 55 | Value: lnwire.MilliSatoshi(500000), 56 | DescriptionHash: []byte("fake 32 byte hash"), 57 | Expiry: 123, 58 | CltvExpiry: 456, 59 | } 60 | 61 | validInvoice := &lnrpc.Invoice{ 62 | Memo: validAddInvoiceData.Memo, 63 | RPreimage: validAddInvoiceData.Preimage[:], 64 | RHash: validAddInvoiceData.Hash[:], 65 | ValueMsat: int64(validAddInvoiceData.Value), 66 | DescriptionHash: validAddInvoiceData.DescriptionHash, 67 | Expiry: validAddInvoiceData.Expiry, 68 | CltvExpiry: validAddInvoiceData.CltvExpiry, 69 | } 70 | 71 | validPayReq := "a valid pay req" 72 | validResp := &lnrpc.AddInvoiceResponse{ 73 | RHash: validRHash[:], 74 | PaymentRequest: validPayReq, 75 | } 76 | 77 | validAddInvoiceArgs := []addInvoiceArg{ 78 | {in: validInvoice}, 79 | } 80 | 81 | validAddInvoice := func(in *lnrpc.Invoice, opts ...grpc.CallOption) ( 82 | *lnrpc.AddInvoiceResponse, error) { 83 | 84 | return validResp, nil 85 | } 86 | 87 | privateAddInvoiceData := *validAddInvoiceData 88 | privateAddInvoiceData.Private = true 89 | privateInvoice := &lnrpc.Invoice{ 90 | Memo: validAddInvoiceData.Memo, 91 | RPreimage: validAddInvoiceData.Preimage[:], 92 | RHash: validAddInvoiceData.Hash[:], 93 | ValueMsat: int64(validAddInvoiceData.Value), 94 | DescriptionHash: validAddInvoiceData.DescriptionHash, 95 | Expiry: validAddInvoiceData.Expiry, 96 | CltvExpiry: validAddInvoiceData.CltvExpiry, 97 | Private: true, 98 | } 99 | privateAddInvoiceArgs := []addInvoiceArg{ 100 | {in: privateInvoice}, 101 | } 102 | 103 | errorAddInvoice := func(in *lnrpc.Invoice, opts ...grpc.CallOption) ( 104 | *lnrpc.AddInvoiceResponse, error) { 105 | 106 | return nil, errors.New("error") 107 | } 108 | 109 | // Set up the test structure. 110 | type expect struct { 111 | addInvoiceArgs []addInvoiceArg 112 | hash lntypes.Hash 113 | payRequest string 114 | wantErr bool 115 | } 116 | 117 | type testCase struct { 118 | name string 119 | client mockRPCClient 120 | invoice *invoicesrpc.AddInvoiceData 121 | expect expect 122 | } 123 | 124 | // Run through the test cases. 125 | tests := []testCase{ 126 | { 127 | name: "happy path", 128 | client: mockRPCClient{ 129 | addInvoice: validAddInvoice, 130 | }, 131 | invoice: validAddInvoiceData, 132 | expect: expect{ 133 | addInvoiceArgs: validAddInvoiceArgs, 134 | hash: validRHash, 135 | payRequest: validPayReq, 136 | }, 137 | }, 138 | { 139 | name: "private invoice", 140 | client: mockRPCClient{ 141 | addInvoice: validAddInvoice, 142 | }, 143 | invoice: &privateAddInvoiceData, 144 | expect: expect{ 145 | addInvoiceArgs: privateAddInvoiceArgs, 146 | hash: validRHash, 147 | payRequest: validPayReq, 148 | }, 149 | }, 150 | { 151 | name: "rpc client error", 152 | client: mockRPCClient{ 153 | addInvoice: errorAddInvoice, 154 | }, 155 | invoice: validAddInvoiceData, 156 | expect: expect{ 157 | addInvoiceArgs: validAddInvoiceArgs, 158 | wantErr: true, 159 | }, 160 | }, 161 | } 162 | 163 | for _, test := range tests { 164 | t.Run(test.name, func(t *testing.T) { 165 | ln := lightningClient{ 166 | client: &test.client, 167 | } 168 | 169 | hash, payRequest, err := ln.AddInvoice( 170 | context.Background(), test.invoice, 171 | ) 172 | 173 | // Check if an error (or no error) was received as 174 | // expected. 175 | if test.expect.wantErr { 176 | require.Error(t, err) 177 | } else { 178 | require.NoError(t, err) 179 | } 180 | 181 | // Check if the expected hash was returned. 182 | require.Equal( 183 | t, hash, test.expect.hash, 184 | "received unexpected hash", 185 | ) 186 | 187 | // Check if the expected invoice was returned. 188 | require.Equal( 189 | t, payRequest, test.expect.payRequest, 190 | "received unexpected payment request", 191 | ) 192 | 193 | // Check if the expected args were passed to the RPC 194 | // client call. 195 | require.Equal(t, test.client.addInvoiceArgs, 196 | test.expect.addInvoiceArgs, 197 | "rpc client call was not made as expected", 198 | ) 199 | }) 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /lnd_services.go: -------------------------------------------------------------------------------- 1 | package lndclient 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "crypto/x509" 7 | "encoding/pem" 8 | "errors" 9 | "fmt" 10 | "net" 11 | "os" 12 | "path/filepath" 13 | "strings" 14 | "time" 15 | 16 | "github.com/btcsuite/btcd/btcutil" 17 | "github.com/btcsuite/btcd/chaincfg" 18 | "github.com/lightningnetwork/lnd/lncfg" 19 | "github.com/lightningnetwork/lnd/lnrpc" 20 | "github.com/lightningnetwork/lnd/lnrpc/verrpc" 21 | "github.com/lightningnetwork/lnd/routing/route" 22 | "google.golang.org/grpc" 23 | "google.golang.org/grpc/codes" 24 | "google.golang.org/grpc/credentials" 25 | "google.golang.org/grpc/status" 26 | ) 27 | 28 | var ( 29 | // defaultRPCTimeout is the default timeout used for rpc calls. 30 | defaultRPCTimeout = 30 * time.Second 31 | 32 | // chainSyncPollInterval is the interval in which we poll the GetInfo 33 | // call to find out if lnd is fully synced to its chain backend. 34 | chainSyncPollInterval = 5 * time.Second 35 | 36 | // minimalCompatibleVersion is the minimum version and build tags 37 | // required in lnd to get all functionality implemented in lndclient. 38 | // Users can provide their own, specific version if needed. If only a 39 | // subset of the lndclient functionality is needed, the required build 40 | // tags can be adjusted accordingly. This default will be used as a 41 | // fallback version if none is specified in the configuration. 42 | minimalCompatibleVersion = &verrpc.Version{ 43 | AppMajor: 0, 44 | AppMinor: 18, 45 | AppPatch: 5, 46 | BuildTags: DefaultBuildTags, 47 | } 48 | 49 | // ErrVersionCheckNotImplemented is the error that is returned if the 50 | // version RPC is not implemented in lnd. This means the version of lnd 51 | // is lower than v0.10.0-beta. 52 | ErrVersionCheckNotImplemented = errors.New("version check not " + 53 | "implemented, need minimum lnd version of v0.10.0-beta") 54 | 55 | // ErrVersionIncompatible is the error that is returned if the connected 56 | // lnd instance is not supported. 57 | ErrVersionIncompatible = errors.New("version incompatible") 58 | 59 | // ErrBuildTagsMissing is the error that is returned if the 60 | // connected lnd instance does not have all built tags activated that 61 | // are required. 62 | ErrBuildTagsMissing = errors.New("build tags missing") 63 | 64 | // DefaultBuildTags is the list of all subserver build tags that are 65 | // required for lndclient to work. 66 | DefaultBuildTags = []string{ 67 | "signrpc", "walletrpc", "chainrpc", "invoicesrpc", 68 | } 69 | 70 | // lnd13UnlockErrors is the list of errors that lnd 0.13 and later 71 | // returns when the wallet is locked or not ready yet. 72 | lnd13UnlockErrors = []string{ 73 | "waiting to start, RPC services not available", 74 | "wallet not created, create one to enable full RPC access", 75 | "wallet locked, unlock it to enable full RPC access", 76 | "the RPC server is in the process of starting up, but not " + 77 | "yet ready to accept calls", 78 | } 79 | ) 80 | 81 | // ServiceClient is an interface that all lnd service clients need to implement. 82 | type ServiceClient[T any] interface { 83 | // RawClientWithMacAuth returns a context with the proper macaroon 84 | // authentication, the default RPC timeout, and the raw client. 85 | RawClientWithMacAuth(parentCtx context.Context) (context.Context, 86 | time.Duration, T) 87 | } 88 | 89 | // LndServicesConfig holds all configuration settings that are needed to connect 90 | // to an lnd node. 91 | type LndServicesConfig struct { 92 | // LndAddress is the network address (host:port) of the lnd node to 93 | // connect to. 94 | LndAddress string 95 | 96 | // Network is the bitcoin network we expect the lnd node to operate on. 97 | Network Network 98 | 99 | // MacaroonDir is the directory where all lnd macaroons can be found. 100 | // Either this, CustomMacaroonPath, or CustomMacaroonHex should be set, 101 | // but only one of them, depending on macaroon preferences. 102 | MacaroonDir string 103 | 104 | // CustomMacaroonPath is the full path to a custom macaroon file. Either 105 | // this, MacaroonDir, or CustomMacaroonHex should be set, but only one 106 | // of them. 107 | CustomMacaroonPath string 108 | 109 | // CustomMacaroonHex is a hexadecimal encoded macaroon string. Either 110 | // this, MacaroonDir, or CustomMacaroonPath should be set, but only 111 | // one of them. 112 | CustomMacaroonHex string 113 | 114 | // TLSPath is the path to lnd's TLS certificate file. Only this or 115 | // TLSData can be set, not both. 116 | TLSPath string 117 | 118 | // TLSData holds the TLS certificate data. Only this or TLSPath can be 119 | // set, not both. 120 | TLSData string 121 | 122 | // Insecure can be checked if we don't need to use tls, such as if 123 | // we're connecting to lnd via a bufconn, then we'll skip verification. 124 | Insecure bool 125 | 126 | // SystemCert specifies whether we'll fallback to a system cert pool 127 | // for tls. 128 | SystemCert bool 129 | 130 | // CheckVersion is the minimum version the connected lnd node needs to 131 | // be in order to be compatible. The node will be checked against this 132 | // when connecting. If no version is supplied, the default minimum 133 | // version will be used. 134 | CheckVersion *verrpc.Version 135 | 136 | // Dialer is an optional dial function that can be passed in if the 137 | // default lncfg.ClientAddressDialer should not be used. 138 | Dialer DialerFunc 139 | 140 | // BlockUntilChainSynced denotes that the NewLndServices function should 141 | // block until the lnd node is fully synced to its chain backend. This 142 | // can take a long time if lnd was offline for a while or if the initial 143 | // block download is still in progress. 144 | BlockUntilChainSynced bool 145 | 146 | // BlockUntilUnlocked denotes that the NewLndServices function should 147 | // block until lnd is unlocked. 148 | BlockUntilUnlocked bool 149 | 150 | // CallerCtx is an optional context that can be passed if the caller 151 | // would like to be able to cancel the long waits involved in starting 152 | // up the client, such as waiting for chain sync to complete when 153 | // BlockUntilChainSynced is set to true, or waiting for lnd to be 154 | // unlocked when BlockUntilUnlocked is set to true. If a context is 155 | // passed in and its Done() channel sends a message, these waits will 156 | // be aborted. This allows a client to still be shut down properly. 157 | CallerCtx context.Context 158 | 159 | // RPCTimeout is an optional custom timeout that will be used for rpc 160 | // calls to lnd. If this value is not set, it will default to 30 161 | // seconds. 162 | RPCTimeout time.Duration 163 | 164 | // ChainSyncPollInterval is the interval in which we poll the GetInfo 165 | // call to find out if lnd is fully synced to its chain backend. 166 | ChainSyncPollInterval time.Duration 167 | } 168 | 169 | // DialerFunc is a function that is used as grpc.WithContextDialer(). 170 | type DialerFunc func(context.Context, string) (net.Conn, error) 171 | 172 | // LndServices constitutes a set of required services. 173 | type LndServices struct { 174 | Client LightningClient 175 | WalletKit WalletKitClient 176 | ChainNotifier ChainNotifierClient 177 | ChainKit ChainKitClient 178 | Signer SignerClient 179 | Invoices InvoicesClient 180 | Router RouterClient 181 | Versioner VersionerClient 182 | State StateClient 183 | 184 | ChainParams *chaincfg.Params 185 | NodeAlias string 186 | NodePubkey route.Vertex 187 | Version *verrpc.Version 188 | ClientConn *grpc.ClientConn 189 | 190 | macaroons macaroonPouch 191 | } 192 | 193 | // GrpcLndServices constitutes a set of required RPC services. 194 | type GrpcLndServices struct { 195 | LndServices 196 | 197 | cleanup func() 198 | } 199 | 200 | // NewLndServices creates a connection to the given lnd instance and creates a 201 | // set of required RPC services. 202 | func NewLndServices(cfg *LndServicesConfig) (*GrpcLndServices, error) { 203 | // We need to use a custom dialer so we can also connect to unix 204 | // sockets and not just TCP addresses. 205 | if cfg.Dialer == nil { 206 | cfg.Dialer = lncfg.ClientAddressDialer(defaultRPCPort) 207 | } 208 | 209 | // Fall back to minimal compatible version if none if specified. 210 | if cfg.CheckVersion == nil { 211 | cfg.CheckVersion = minimalCompatibleVersion 212 | } 213 | 214 | // Of the macaroon directory, the custom macaroon path, and the custom 215 | // macaroon hex, we only allow one to be set at once. If all are empty, 216 | // that's fine, the default behavior is to use lnd's default directory 217 | // to try to locate the macaroons. 218 | macaroonOptions := []string{ 219 | cfg.MacaroonDir, 220 | cfg.CustomMacaroonPath, 221 | cfg.CustomMacaroonHex, 222 | } 223 | macOptionCount := 0 224 | for _, option := range macaroonOptions { 225 | if option != "" { 226 | macOptionCount++ 227 | } 228 | } 229 | if macOptionCount > 1 { 230 | return nil, fmt.Errorf("must set only one: MacaroonDir, " + 231 | "CustomMacaroonPath, or CustomMacaroonHex") 232 | } 233 | 234 | // Based on the network, if the macaroon directory isn't set, then 235 | // we'll use the expected default locations. 236 | macaroonDir := cfg.MacaroonDir 237 | if macaroonDir == "" { 238 | switch cfg.Network { 239 | case NetworkTestnet: 240 | macaroonDir = filepath.Join( 241 | defaultLndDir, defaultDataDir, 242 | defaultChainSubDir, "bitcoin", "testnet", 243 | ) 244 | 245 | case NetworkTestnet4: 246 | macaroonDir = filepath.Join( 247 | defaultLndDir, defaultDataDir, 248 | defaultChainSubDir, "bitcoin", "testnet4", 249 | ) 250 | 251 | case NetworkMainnet: 252 | macaroonDir = filepath.Join( 253 | defaultLndDir, defaultDataDir, 254 | defaultChainSubDir, "bitcoin", "mainnet", 255 | ) 256 | 257 | case NetworkSimnet: 258 | macaroonDir = filepath.Join( 259 | defaultLndDir, defaultDataDir, 260 | defaultChainSubDir, "bitcoin", "simnet", 261 | ) 262 | 263 | case NetworkSignet: 264 | macaroonDir = filepath.Join( 265 | defaultLndDir, defaultDataDir, 266 | defaultChainSubDir, "bitcoin", "signet", 267 | ) 268 | 269 | case NetworkRegtest: 270 | macaroonDir = filepath.Join( 271 | defaultLndDir, defaultDataDir, 272 | defaultChainSubDir, "bitcoin", "regtest", 273 | ) 274 | 275 | default: 276 | return nil, fmt.Errorf("unsupported network: %v", 277 | cfg.Network) 278 | } 279 | } 280 | 281 | // Setup connection with lnd 282 | log.Infof("Creating lnd connection to %v", cfg.LndAddress) 283 | conn, err := getClientConn(cfg) 284 | if err != nil { 285 | return nil, err 286 | } 287 | 288 | log.Infof("Connected to lnd") 289 | 290 | chainParams, err := cfg.Network.ChainParams() 291 | if err != nil { 292 | return nil, err 293 | } 294 | 295 | // We are going to check that the connected lnd is on the same network 296 | // and is a compatible version with all the required subservers enabled. 297 | // For this, we make two calls, both of which only need the readonly 298 | // macaroon. We don't use the pouch yet because if not all subservers 299 | // are enabled, then not all macaroons might be there and the user would 300 | // get a more cryptic error message. 301 | var readonlyMac serializedMacaroon 302 | if cfg.CustomMacaroonHex != "" { 303 | readonlyMac = serializedMacaroon(cfg.CustomMacaroonHex) 304 | } else { 305 | readonlyMac, err = loadMacaroon( 306 | macaroonDir, string(ReadOnlyServiceMac), 307 | cfg.CustomMacaroonPath, 308 | ) 309 | if err != nil { 310 | return nil, err 311 | } 312 | } 313 | 314 | timeout := defaultRPCTimeout 315 | if cfg.RPCTimeout != 0 { 316 | timeout = cfg.RPCTimeout 317 | } 318 | 319 | if cfg.ChainSyncPollInterval == 0 { 320 | cfg.ChainSyncPollInterval = chainSyncPollInterval 321 | } 322 | 323 | basicClient := lnrpc.NewLightningClient(conn) 324 | stateClient := newStateClient(conn, readonlyMac, timeout) 325 | versionerClient := newVersionerClient(conn, readonlyMac, timeout) 326 | 327 | cleanupConn := func() { 328 | closeErr := conn.Close() 329 | if closeErr != nil { 330 | log.Errorf("Error closing lnd connection: %v", closeErr) 331 | } 332 | } 333 | 334 | // Get lnd's info, blocking until lnd is unlocked if required. 335 | info, err := getLndInfo( 336 | cfg.CallerCtx, basicClient, readonlyMac, stateClient, 337 | cfg.BlockUntilUnlocked, 338 | ) 339 | if err != nil { 340 | cleanupConn() 341 | return nil, err 342 | } 343 | 344 | nodeAlias, nodeKey, version, err := checkLndCompatibility( 345 | conn, readonlyMac, info, cfg.Network, cfg.CheckVersion, timeout, 346 | ) 347 | if err != nil { 348 | cleanupConn() 349 | return nil, err 350 | } 351 | 352 | // Now that we've ensured our macaroon directory is set properly, we 353 | // can retrieve our full macaroon pouch from the directory. 354 | macaroons, err := newMacaroonPouch( 355 | macaroonDir, cfg.CustomMacaroonPath, cfg.CustomMacaroonHex, 356 | ) 357 | if err != nil { 358 | cleanupConn() 359 | return nil, fmt.Errorf("unable to obtain macaroons: %v", err) 360 | } 361 | 362 | // With the macaroons loaded and the version checked, we can now create 363 | // the real lightning client which uses the admin macaroon. 364 | lightningClient := newLightningClient( 365 | conn, timeout, chainParams, macaroons[AdminServiceMac], 366 | ) 367 | 368 | // With the network check passed, we'll now initialize the rest of the 369 | // sub-server connections, giving each of them their specific macaroon. 370 | notifierClient := newChainNotifierClient( 371 | conn, macaroons[ChainNotifierServiceMac], timeout, 372 | ) 373 | chainKitClient := newChainKitClient( 374 | conn, macaroons[ChainNotifierServiceMac], timeout, 375 | ) 376 | signerClient := newSignerClient( 377 | conn, macaroons[SignerServiceMac], timeout, 378 | ) 379 | walletKitClient := newWalletKitClient( 380 | conn, macaroons[WalletKitServiceMac], timeout, chainParams, 381 | ) 382 | invoicesClient := newInvoicesClient( 383 | conn, macaroons[InvoiceServiceMac], timeout, 384 | ) 385 | routerClient := newRouterClient( 386 | conn, macaroons[RouterServiceMac], timeout, 387 | ) 388 | 389 | cleanup := func() { 390 | log.Debugf("Closing lnd connection") 391 | cleanupConn() 392 | 393 | log.Debugf("Wait for client to finish") 394 | lightningClient.WaitForFinished() 395 | 396 | log.Debugf("Wait for chain notifier to finish") 397 | notifierClient.WaitForFinished() 398 | 399 | log.Debugf("Wait for invoices to finish") 400 | invoicesClient.WaitForFinished() 401 | 402 | log.Debugf("Wait for router to finish") 403 | routerClient.WaitForFinished() 404 | 405 | log.Debugf("Lnd services finished") 406 | } 407 | 408 | services := &GrpcLndServices{ 409 | LndServices: LndServices{ 410 | Client: lightningClient, 411 | WalletKit: walletKitClient, 412 | ChainNotifier: notifierClient, 413 | ChainKit: chainKitClient, 414 | Signer: signerClient, 415 | Invoices: invoicesClient, 416 | Router: routerClient, 417 | Versioner: versionerClient, 418 | State: stateClient, 419 | ChainParams: chainParams, 420 | NodeAlias: nodeAlias, 421 | NodePubkey: route.Vertex(nodeKey), 422 | Version: version, 423 | ClientConn: conn, 424 | macaroons: macaroons, 425 | }, 426 | cleanup: cleanup, 427 | } 428 | 429 | log.Infof("Using network %v", cfg.Network) 430 | 431 | // If requested in the configuration, we now wait for lnd to fully sync 432 | // to its chain backend. We do not add any timeout as it would be hard 433 | // to determine a sane value. If the initial block download is still in 434 | // progress, this could take hours. 435 | if cfg.BlockUntilChainSynced { 436 | log.Infof("Waiting for lnd to be fully synced to its chain " + 437 | "backend, this might take a while") 438 | 439 | err := services.waitForChainSync( 440 | cfg.CallerCtx, timeout, cfg.ChainSyncPollInterval, 441 | ) 442 | if err != nil { 443 | cleanup() 444 | return nil, fmt.Errorf("error waiting for chain to "+ 445 | "be synced: %v", err) 446 | } 447 | 448 | log.Infof("lnd is now fully synced to its chain backend") 449 | } 450 | 451 | return services, nil 452 | } 453 | 454 | // WithMacaroonAuthForService modifies the passed context to include the 455 | // macaroon KV metadata for the target Service. This method can be used to 456 | // add the macaroon at call time, rather than when the connection to 457 | // the gRPC server is created. 458 | func (s *LndServices) WithMacaroonAuthForService(ctx context.Context, 459 | service LnrpcServiceMac) (context.Context, error) { 460 | 461 | mac, ok := s.macaroons[service] 462 | if !ok { 463 | return nil, fmt.Errorf("unknown service %v", service) 464 | } 465 | 466 | return mac.WithMacaroonAuth(ctx), nil 467 | } 468 | 469 | // Close closes the lnd connection and waits for all sub server clients to 470 | // finish their goroutines. 471 | func (s *GrpcLndServices) Close() { 472 | s.cleanup() 473 | 474 | log.Debugf("Lnd services finished") 475 | } 476 | 477 | // waitForChainSync waits and blocks until the connected lnd node is fully 478 | // synced to its chain backend. This could theoretically take hours if the 479 | // initial block download is still in progress. 480 | func (s *GrpcLndServices) waitForChainSync(ctx context.Context, 481 | timeout, pollInterval time.Duration) error { 482 | 483 | mainCtx := ctx 484 | if mainCtx == nil { 485 | mainCtx = context.Background() 486 | } 487 | 488 | // We spawn a goroutine that polls in regular intervals and reports back 489 | // once the chain is ready (or something went wrong). If the chain is 490 | // already synced, this should return almost immediately. 491 | update := make(chan error) 492 | go func() { 493 | for { 494 | // The GetInfo call can take a while. But if it takes 495 | // too long, that can be a sign of something being wrong 496 | // with the node. That's why we don't wait any longer 497 | // than a few seconds for each individual GetInfo call. 498 | ctxt, cancel := context.WithTimeout(mainCtx, timeout) 499 | info, err := s.Client.GetInfo(ctxt) 500 | if err != nil { 501 | cancel() 502 | update <- fmt.Errorf("error in GetInfo call: "+ 503 | "%v", err) 504 | return 505 | } 506 | cancel() 507 | 508 | // We're done, deliver a nil update by closing the chan. 509 | if info.SyncedToChain { 510 | close(update) 511 | return 512 | } 513 | 514 | select { 515 | // If we're not yet done, let's now wait a few seconds. 516 | case <-time.After(pollInterval): 517 | 518 | // If the user cancels the context, we should also 519 | // abort the wait. 520 | case <-mainCtx.Done(): 521 | update <- mainCtx.Err() 522 | return 523 | } 524 | } 525 | }() 526 | 527 | // Wait for either an error or the nil close signal to arrive. 528 | return <-update 529 | } 530 | 531 | // getLndInfo queries lnd for information about the node it is connected to. 532 | // If the waitForUnlocked boolean is set, it will examine any errors returned 533 | // and back off if the failure is due to lnd currently being locked. Otherwise, 534 | // it will fail fast on any errors returned. We use the raw ln client so that 535 | // we can set specific grpc options we need to wait for lnd to be ready. 536 | func getLndInfo(ctx context.Context, basicClient lnrpc.LightningClient, 537 | readonlyMac serializedMacaroon, stateClient StateClient, 538 | waitForUnlocked bool) (*Info, error) { 539 | 540 | if ctx == nil { 541 | ctx = context.Background() 542 | } 543 | 544 | // We expect the initial connection to already have succeeded. So we 545 | // know lnd is responding. Therefore we can now just subscribe to the 546 | // state update RPC. With the new unified unlocker RPC the server 547 | // shouldn't shut down/close on us during the unlocking phase. 548 | // 549 | // All we have to do is to interpret the states that lnd could be in 550 | // during the unlock: 551 | // Locked: lnd is currently locked or no wallet exist 552 | // -> WalletStateNonExisting, WalletStateLocked 553 | // Unlocking: lnd has just been unlocked, -> WalletStateUnlocked 554 | // Unlocked, ok: lnd is unlocked and ready 555 | // -> WalletStateRPCActive 556 | // Unlocked, not ok: lnd is unlocked but in bad state, err 557 | stateChan, errChan, err := stateClient.SubscribeState(ctx) 558 | if err != nil { 559 | // Because we're expecting lnd to be at least version 0.13 here 560 | // we would get an "Unimplemented" error here if it's a previous 561 | // version and the node is locked. Since the actual version 562 | // compatibility check only runs after this check, we want to 563 | // print at least a somewhat useful error message here. 564 | if IsUnlockError(err) { 565 | err = fmt.Errorf("lnd version incompatible, need "+ 566 | "at least v0.13.0-beta, got error on "+ 567 | "state subscription: %w", err) 568 | } 569 | 570 | return nil, fmt.Errorf("error subscribing to lnd wallet "+ 571 | "state: %w", err) 572 | } 573 | 574 | getInfo := func() (*Info, error) { 575 | // We've made a connection and (possibly) unlocked lnd. All that 576 | // is left to do is to query and return the node information. 577 | info, err := basicClient.GetInfo( 578 | readonlyMac.WithMacaroonAuth(ctx), 579 | &lnrpc.GetInfoRequest{}, 580 | ) 581 | if err != nil { 582 | return nil, err 583 | } 584 | 585 | return newInfo(info) 586 | } 587 | 588 | // If we don't want to wait for the unlock we exit the function early 589 | // and will try to return the node info below. This could fail if the 590 | // node is indeed still locked so this flag doesn't make a lot of sense 591 | // anymore... 592 | if !waitForUnlocked { 593 | return getInfo() 594 | } 595 | 596 | // If we do want to wait for the unlock, we need to consume the state 597 | // updates now. 598 | log.Info("Waiting for lnd to unlock") 599 | for { 600 | select { 601 | case state, ok := <-stateChan: 602 | // The update channel was closed, which signifies that the 603 | // wallet/daemon is now fully ready, so we can just return 604 | // the GetInfo response. 605 | if !ok { 606 | return getInfo() 607 | } 608 | 609 | log.Infof("Wallet state of lnd is now: %v", state) 610 | 611 | // Once we reach the final state we can break out of the 612 | // loop. We also need to be backward compatible to nodes 613 | // running 0.13.x which only had the RPC active state. 614 | if state.ReadyForGetInfo() { 615 | return getInfo() 616 | } 617 | 618 | case err, ok := <-errChan: 619 | // The channel was closed by the main state client. In 620 | // this case the system is fully ready, so we'll ignore 621 | // this as it isn't actually an error. 622 | if !ok { 623 | // We can just return get info as is, since we 624 | // know the daemon is fully ready at this 625 | // point. 626 | return getInfo() 627 | } 628 | 629 | log.Errorf("Error while waiting for lnd to be "+ 630 | "unlocked: %v", err) 631 | return nil, err 632 | 633 | case <-ctx.Done(): 634 | return nil, ctx.Err() 635 | } 636 | } 637 | } 638 | 639 | // IsUnlockError returns true if the given error is one that lnd returns when 640 | // its wallet is locked, either before version 0.13 or after. 641 | func IsUnlockError(err error) bool { 642 | if err == nil { 643 | return false 644 | } 645 | 646 | // Is the error one that lnd 0.13 returns when it's locked? 647 | errStr := err.Error() 648 | for _, lnd13Err := range lnd13UnlockErrors { 649 | if strings.Contains(errStr, lnd13Err) { 650 | return true 651 | } 652 | } 653 | 654 | // If we do not get a rpc error code, something else is wrong with the 655 | // call, so we fail. 656 | rpcErrorCode, ok := status.FromError(err) 657 | if !ok { 658 | return false 659 | } 660 | 661 | // Unimplemented means we're hitting the GetInfo RPC while the wallet 662 | // unlocker RPC is still up. Unavailable can be returned in the short 663 | // window of time while the unlocker shuts down and the main RPC server 664 | // is started. 665 | if rpcErrorCode.Code() == codes.Unimplemented || 666 | rpcErrorCode.Code() == codes.Unavailable { 667 | 668 | return true 669 | } 670 | 671 | return false 672 | } 673 | 674 | // checkLndCompatibility makes sure the connected lnd instance is running on the 675 | // correct network, has the version RPC implemented, is the correct minimal 676 | // version and supports all required build tags/subservers. 677 | func checkLndCompatibility(conn grpc.ClientConnInterface, 678 | readonlyMac serializedMacaroon, info *Info, network Network, 679 | minVersion *verrpc.Version, timeout time.Duration) (string, 680 | [33]byte, *verrpc.Version, error) { 681 | 682 | // onErr is a closure that simplifies returning multiple values in the 683 | // error case. 684 | onErr := func(err error) (string, [33]byte, *verrpc.Version, error) { 685 | // Make static error messages a bit less cryptic by adding the 686 | // version or build tag that we expect. 687 | newErr := fmt.Errorf("lnd compatibility check failed: %v", err) 688 | if err == ErrVersionIncompatible || err == ErrBuildTagsMissing { 689 | newErr = fmt.Errorf("error checking connected lnd "+ 690 | "version. at least version \"%s\" is "+ 691 | "required", VersionString(minVersion)) 692 | } 693 | 694 | return "", [33]byte{}, nil, newErr 695 | } 696 | 697 | // Ensure that the network for lnd matches our expected network. 698 | if string(network) != info.Network { 699 | err := fmt.Errorf("network mismatch with connected lnd node, "+ 700 | "wanted '%s', got '%s'", network, info.Network) 701 | return onErr(err) 702 | } 703 | 704 | // We use our own clients with a readonly macaroon here, because we know 705 | // that's all we need for the checks. 706 | versionerClient := newVersionerClient(conn, readonlyMac, timeout) 707 | 708 | // Now let's also check the version of the connected lnd node. 709 | version, err := checkVersionCompatibility(versionerClient, minVersion) 710 | if err != nil { 711 | return onErr(err) 712 | } 713 | 714 | // Return the static part of the info we just queried from the node so 715 | // it can be cached for later use. 716 | return info.Alias, info.IdentityPubkey, version, nil 717 | } 718 | 719 | // checkVersionCompatibility makes sure the connected lnd node has the correct 720 | // version and required build tags enabled. 721 | // 722 | // NOTE: This check will **never** return a non-nil error for a version of 723 | // lnd < 0.10.0 because any version previous to 0.10.0 doesn't have the version 724 | // endpoint implemented! 725 | func checkVersionCompatibility(client VersionerClient, 726 | expected *verrpc.Version) (*verrpc.Version, error) { 727 | 728 | // First, test that the version RPC is even implemented. 729 | version, err := client.GetVersion(context.Background()) 730 | if err != nil { 731 | // The version service has only been added in lnd v0.10.0. If 732 | // we get an unimplemented error, it means the lnd version is 733 | // definitely older than that. 734 | s, ok := status.FromError(err) 735 | if ok && s.Code() == codes.Unimplemented { 736 | return nil, ErrVersionCheckNotImplemented 737 | } 738 | return nil, fmt.Errorf("GetVersion error: %v", err) 739 | } 740 | 741 | log.Infof("lnd version: %v", VersionString(version)) 742 | 743 | // Now check the version and make sure all required build tags are set. 744 | err = AssertVersionCompatible(version, expected) 745 | if err != nil { 746 | return nil, err 747 | } 748 | err = assertBuildTagsEnabled(version, expected.BuildTags) 749 | if err != nil { 750 | return nil, err 751 | } 752 | 753 | // All check positive, version is fully compatible. 754 | return version, nil 755 | } 756 | 757 | // AssertVersionCompatible makes sure the detected lnd version is compatible 758 | // with our current version requirements. 759 | func AssertVersionCompatible(actual *verrpc.Version, 760 | expected *verrpc.Version) error { 761 | 762 | // We need to check the versions parts sequentially as they are 763 | // hierarchical. 764 | if actual.AppMajor != expected.AppMajor { 765 | if actual.AppMajor > expected.AppMajor { 766 | return nil 767 | } 768 | return ErrVersionIncompatible 769 | } 770 | 771 | if actual.AppMinor != expected.AppMinor { 772 | if actual.AppMinor > expected.AppMinor { 773 | return nil 774 | } 775 | return ErrVersionIncompatible 776 | } 777 | 778 | if actual.AppPatch != expected.AppPatch { 779 | if actual.AppPatch > expected.AppPatch { 780 | return nil 781 | } 782 | return ErrVersionIncompatible 783 | } 784 | 785 | // The actual version and expected version are identical. 786 | return nil 787 | } 788 | 789 | // assertBuildTagsEnabled makes sure all required build tags are set. 790 | func assertBuildTagsEnabled(actual *verrpc.Version, 791 | requiredTags []string) error { 792 | 793 | tagMap := make(map[string]struct{}) 794 | for _, tag := range actual.BuildTags { 795 | tagMap[tag] = struct{}{} 796 | } 797 | for _, required := range requiredTags { 798 | if _, ok := tagMap[required]; !ok { 799 | return ErrBuildTagsMissing 800 | } 801 | } 802 | 803 | // All tags found. 804 | return nil 805 | } 806 | 807 | var ( 808 | defaultRPCPort = "10009" 809 | defaultLndDir = btcutil.AppDataDir("lnd", false) 810 | defaultTLSCertFilename = "tls.cert" 811 | defaultTLSCertPath = filepath.Join( 812 | defaultLndDir, defaultTLSCertFilename, 813 | ) 814 | defaultDataDir = "data" 815 | defaultChainSubDir = "chain" 816 | 817 | // maxMsgRecvSize is the largest gRPC message our client will receive. 818 | // We set this to 800MiB. 819 | maxMsgRecvSize = grpc.MaxCallRecvMsgSize(800 * 1024 * 1024) 820 | ) 821 | 822 | func getClientConn(cfg *LndServicesConfig) (*grpc.ClientConn, error) { 823 | creds, err := GetTLSCredentials( 824 | cfg.TLSData, cfg.TLSPath, cfg.Insecure, cfg.SystemCert, 825 | ) 826 | if err != nil { 827 | return nil, fmt.Errorf("unable to get tls creds: %v", err) 828 | } 829 | 830 | // Create a dial options array. 831 | opts := []grpc.DialOption{ 832 | grpc.WithTransportCredentials(creds), 833 | 834 | // Use a custom dialer, to allow connections to unix sockets, 835 | // in-memory listeners etc, and not just TCP addresses. 836 | grpc.WithContextDialer(cfg.Dialer), 837 | grpc.WithDefaultCallOptions(maxMsgRecvSize), 838 | } 839 | 840 | conn, err := grpc.Dial(cfg.LndAddress, opts...) 841 | if err != nil { 842 | return nil, fmt.Errorf("unable to connect to RPC server: %v", 843 | err) 844 | } 845 | 846 | return conn, nil 847 | } 848 | 849 | // GetTLSCredentials gets the tls credentials, whether provided as straight-up 850 | // data or a path to a certificate file. 851 | func GetTLSCredentials(tlsData, tlsPath string, insecure, 852 | systemCert bool) (credentials.TransportCredentials, error) { 853 | 854 | // We'll determine if the tls certificate is passed in directly as 855 | // data, by a path, or try the system's certificate chain, and then 856 | // load it. 857 | var creds credentials.TransportCredentials 858 | switch { 859 | case tlsPath != "" && tlsData != "": 860 | return nil, fmt.Errorf("must set only one: TLSPath or TLSData") 861 | 862 | case insecure && systemCert: 863 | return nil, fmt.Errorf("cannot set insecure and system cert " + 864 | "at the same time") 865 | 866 | case insecure: 867 | // If we don't need to use tls, such as if we're connecting to 868 | // lnd via a bufconn, then we'll skip verification. 869 | creds = credentials.NewTLS(&tls.Config{ 870 | InsecureSkipVerify: true, // nolint:gosec 871 | }) 872 | 873 | case systemCert: 874 | // Fallback to the system pool. Using an empty tls config is an 875 | // alternative to x509.SystemCertPool(), which is not supported 876 | // on Windows. 877 | creds = credentials.NewTLS(&tls.Config{}) 878 | 879 | case tlsData != "": 880 | tlsBytes := []byte(tlsData) 881 | 882 | block, _ := pem.Decode(tlsBytes) 883 | if block == nil || block.Type != "CERTIFICATE" { 884 | return nil, errors.New("failed to decode PEM block " + 885 | "containing tls certificate") 886 | } 887 | 888 | cert, err := x509.ParseCertificate(block.Bytes) 889 | if err != nil { 890 | return nil, err 891 | } 892 | 893 | pool := x509.NewCertPool() 894 | pool.AddCert(cert) 895 | 896 | // Load the specified TLS certificate and build transport 897 | // credentials. 898 | creds = credentials.NewClientTLSFromCert(pool, "") 899 | 900 | case tlsPath != "": 901 | var err error 902 | creds, err = credentials.NewClientTLSFromFile(tlsPath, "") 903 | if err != nil { 904 | return nil, err 905 | } 906 | 907 | default: 908 | // If neither tlsData nor tlsPath were set, we'll try the 909 | // default lnd tls cert path. 910 | _, err := os.Stat(defaultTLSCertPath) 911 | if err != nil { 912 | return nil, fmt.Errorf("couldn't find out if default "+ 913 | "lnd TLS cert at %s exists: %v", 914 | defaultTLSCertPath, err) 915 | } 916 | creds, err = credentials.NewClientTLSFromFile( 917 | defaultTLSCertPath, "", 918 | ) 919 | if err != nil { 920 | return nil, fmt.Errorf("couldn't load default lnd "+ 921 | "TLS cert at %s: %v", defaultTLSCertPath, err) 922 | } 923 | } 924 | 925 | return creds, nil 926 | } 927 | -------------------------------------------------------------------------------- /lnd_services_test.go: -------------------------------------------------------------------------------- 1 | package lndclient 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | "time" 8 | 9 | "github.com/lightningnetwork/lnd/lnrpc" 10 | "github.com/lightningnetwork/lnd/lnrpc/verrpc" 11 | "github.com/stretchr/testify/require" 12 | "google.golang.org/grpc" 13 | "google.golang.org/grpc/codes" 14 | "google.golang.org/grpc/status" 15 | ) 16 | 17 | type mockVersioner struct { 18 | version *verrpc.Version 19 | err error 20 | } 21 | 22 | // RawClientWithMacAuth returns a context with the proper macaroon 23 | // authentication, the default RPC timeout, and the raw client. 24 | func (m *mockVersioner) RawClientWithMacAuth( 25 | parentCtx context.Context) (context.Context, time.Duration, 26 | verrpc.VersionerClient) { 27 | 28 | return parentCtx, 0, nil 29 | } 30 | 31 | func (m *mockVersioner) GetVersion(_ context.Context) (*verrpc.Version, error) { 32 | return m.version, m.err 33 | } 34 | 35 | // TestCheckVersionCompatibility makes sure the correct error is returned if an 36 | // old lnd is connected that doesn't implement the version RPC, has an older 37 | // version or if an lnd with not all subservers enabled is connected. 38 | func TestCheckVersionCompatibility(t *testing.T) { 39 | // Make sure a version check against a node that doesn't implement the 40 | // version RPC always fails. 41 | unimplemented := &mockVersioner{ 42 | err: status.Error(codes.Unimplemented, "missing"), 43 | } 44 | _, err := checkVersionCompatibility(unimplemented, &verrpc.Version{ 45 | AppMajor: 0, 46 | AppMinor: 10, 47 | AppPatch: 0, 48 | }) 49 | if err != ErrVersionCheckNotImplemented { 50 | t.Fatalf("unexpected error. got '%v' wanted '%v'", err, 51 | ErrVersionCheckNotImplemented) 52 | } 53 | 54 | // Next, make sure an older version than what we want is rejected. 55 | oldVersion := &mockVersioner{ 56 | version: &verrpc.Version{ 57 | AppMajor: 0, 58 | AppMinor: 10, 59 | AppPatch: 0, 60 | }, 61 | } 62 | _, err = checkVersionCompatibility(oldVersion, &verrpc.Version{ 63 | AppMajor: 0, 64 | AppMinor: 11, 65 | AppPatch: 0, 66 | }) 67 | if err != ErrVersionIncompatible { 68 | t.Fatalf("unexpected error. got '%v' wanted '%v'", err, 69 | ErrVersionIncompatible) 70 | } 71 | 72 | // Finally, make sure we also get the correct error when trying to run 73 | // against an lnd that doesn't have all required build tags enabled. 74 | buildTagsMissing := &mockVersioner{ 75 | version: &verrpc.Version{ 76 | AppMajor: 0, 77 | AppMinor: 10, 78 | AppPatch: 0, 79 | BuildTags: []string{"dev", "lntest", "btcd", "signrpc"}, 80 | }, 81 | } 82 | _, err = checkVersionCompatibility(buildTagsMissing, &verrpc.Version{ 83 | AppMajor: 0, 84 | AppMinor: 10, 85 | AppPatch: 0, 86 | BuildTags: []string{"signrpc", "walletrpc"}, 87 | }) 88 | if err != ErrBuildTagsMissing { 89 | t.Fatalf("unexpected error. got '%v' wanted '%v'", err, 90 | ErrVersionIncompatible) 91 | } 92 | } 93 | 94 | // TestLndVersionCheckComparison makes sure the version check comparison works 95 | // correctly and considers all three version levels. 96 | func TestLndVersionCheckComparison(t *testing.T) { 97 | actual := &verrpc.Version{ 98 | AppMajor: 1, 99 | AppMinor: 2, 100 | AppPatch: 3, 101 | } 102 | testCases := []struct { 103 | name string 104 | expectMajor uint32 105 | expectMinor uint32 106 | expectPatch uint32 107 | actual *verrpc.Version 108 | expectedErr error 109 | }{ 110 | { 111 | name: "no expectation", 112 | expectMajor: 0, 113 | expectMinor: 0, 114 | expectPatch: 0, 115 | actual: actual, 116 | expectedErr: nil, 117 | }, 118 | { 119 | name: "expect exact same version", 120 | expectMajor: 1, 121 | expectMinor: 2, 122 | expectPatch: 3, 123 | actual: actual, 124 | expectedErr: nil, 125 | }, 126 | { 127 | name: "ignore patch if minor is bigger", 128 | expectMajor: 12, 129 | expectMinor: 9, 130 | expectPatch: 14, 131 | actual: &verrpc.Version{ 132 | AppMajor: 12, 133 | AppMinor: 22, 134 | AppPatch: 0, 135 | }, 136 | expectedErr: nil, 137 | }, 138 | { 139 | name: "all fields different", 140 | expectMajor: 3, 141 | expectMinor: 2, 142 | expectPatch: 1, 143 | actual: actual, 144 | expectedErr: ErrVersionIncompatible, 145 | }, 146 | { 147 | name: "patch version different", 148 | expectMajor: 1, 149 | expectMinor: 2, 150 | expectPatch: 4, 151 | actual: actual, 152 | expectedErr: ErrVersionIncompatible, 153 | }, 154 | } 155 | 156 | for _, tc := range testCases { 157 | t.Run(tc.name, func(t *testing.T) { 158 | err := AssertVersionCompatible( 159 | tc.actual, &verrpc.Version{ 160 | AppMajor: tc.expectMajor, 161 | AppMinor: tc.expectMinor, 162 | AppPatch: tc.expectPatch, 163 | }, 164 | ) 165 | if err != tc.expectedErr { 166 | t.Fatalf("unexpected error, got '%v' wanted "+ 167 | "'%v'", err, tc.expectedErr) 168 | } 169 | }) 170 | } 171 | } 172 | 173 | // lockLNDMock is a mock lightning client which mocks calls to getinfo to 174 | // determine the unlocked state of lnd. 175 | type lockLNDMock struct { 176 | lnrpc.LightningClient 177 | StateClient 178 | callCount int 179 | errors []error 180 | stateErr error 181 | states []WalletState 182 | } 183 | 184 | // GetInfo mocks a call to getinfo, using our call count to get the error for 185 | // this call as the index in our pre-set error slice. 186 | func (l *lockLNDMock) GetInfo(ctx context.Context, _ *lnrpc.GetInfoRequest, 187 | _ ...grpc.CallOption) (*lnrpc.GetInfoResponse, error) { 188 | 189 | // Our actual call would use ctx, so add a panic to reflect that. 190 | if ctx == nil { 191 | panic("nil context for getinfo") 192 | } 193 | 194 | err := l.errors[l.callCount] 195 | 196 | l.callCount++ 197 | 198 | return &lnrpc.GetInfoResponse{ 199 | Chains: []*lnrpc.Chain{{}}, 200 | }, err 201 | } 202 | 203 | func (l *lockLNDMock) SubscribeState(context.Context) (chan WalletState, 204 | chan error, error) { 205 | 206 | if l.stateErr != nil { 207 | return nil, nil, l.stateErr 208 | } 209 | 210 | stateChan := make(chan WalletState, 1) 211 | errChan := make(chan error, 1) 212 | 213 | go func() { 214 | for _, state := range l.states { 215 | stateChan <- state 216 | 217 | // If this is the final state, no more states will be 218 | // sent to us and we can close the subscription. 219 | if state == WalletStateRPCActive { 220 | close(stateChan) 221 | close(errChan) 222 | 223 | return 224 | } 225 | } 226 | }() 227 | 228 | return stateChan, errChan, nil 229 | } 230 | 231 | func newLockLndMock(errors []error, stateErr error, 232 | states []WalletState) *lockLNDMock { 233 | 234 | return &lockLNDMock{ 235 | errors: errors, 236 | stateErr: stateErr, 237 | states: states, 238 | } 239 | } 240 | 241 | // TestGetLndInfo tests our logic for querying lnd for information in the case 242 | // where we wait for the wallet to unlock, and when we fail fast. 243 | func TestGetLndInfo(t *testing.T) { 244 | var ( 245 | ctx = context.Background() 246 | nonNilErr = errors.New("failed") 247 | unlockErr = status.Error(codes.Unimplemented, "unimpl") 248 | ) 249 | 250 | tests := []struct { 251 | name string 252 | context context.Context 253 | waitUnlocked bool 254 | stateErr error 255 | states []WalletState 256 | errors []error 257 | expected error 258 | }{ 259 | { 260 | name: "no error", 261 | context: ctx, 262 | errors: []error{nil}, 263 | states: []WalletState{ 264 | WalletStateWaitingToStart, 265 | WalletStateLocked, 266 | WalletStateUnlocked, 267 | WalletStateRPCActive, 268 | }, 269 | expected: nil, 270 | }, 271 | { 272 | name: "nil context", 273 | errors: []error{ 274 | nil, 275 | }, 276 | states: []WalletState{ 277 | WalletStateRPCActive, 278 | }, 279 | expected: nil, 280 | }, 281 | { 282 | name: "do not wait for unlock", 283 | errors: []error{unlockErr}, 284 | expected: unlockErr, 285 | }, 286 | { 287 | name: "wait for unlock", 288 | waitUnlocked: true, 289 | errors: []error{nil}, 290 | states: []WalletState{ 291 | WalletStateRPCActive, 292 | }, 293 | expected: nil, 294 | }, 295 | { 296 | name: "lnd down", 297 | waitUnlocked: true, 298 | stateErr: context.DeadlineExceeded, 299 | expected: context.DeadlineExceeded, 300 | }, 301 | { 302 | name: "other error", 303 | waitUnlocked: true, 304 | errors: []error{nonNilErr}, 305 | states: []WalletState{ 306 | WalletStateRPCActive, 307 | }, 308 | expected: nonNilErr, 309 | }, 310 | } 311 | 312 | for _, test := range tests { 313 | t.Run(test.name, func(t *testing.T) { 314 | mock := newLockLndMock( 315 | test.errors, test.stateErr, test.states, 316 | ) 317 | 318 | _, err := getLndInfo( 319 | test.context, mock, "readonlymac", mock, 320 | test.waitUnlocked, 321 | ) 322 | 323 | if test.expected == nil { 324 | require.NoError(t, err) 325 | } else { 326 | require.Error(t, err) 327 | 328 | // Error might be wrapped. 329 | require.ErrorIs(t, err, test.expected) 330 | } 331 | }) 332 | } 333 | } 334 | 335 | // TestCustomMacaroonHex tests that the macaroon pouch properly takes in a 336 | // macaroon provided in hex string format. 337 | func TestCustomMacaroonHex(t *testing.T) { 338 | dummyMacStr := "0201047465737402067788991234560000062052d26ed139ea5af8" + 339 | "3e675500c4ccb2471f62191b745bab820f129e5588a255d2" 340 | 341 | // Test that MacaroonPouch adds the macaroon hex string properly. 342 | macaroons, err := newMacaroonPouch( 343 | "", "", dummyMacStr, 344 | ) 345 | require.NoError(t, err) 346 | 347 | require.Equal( 348 | t, macaroons[InvoiceServiceMac], serializedMacaroon(dummyMacStr), 349 | "macaroon hex string not set correctly", 350 | ) 351 | 352 | // If both CustomMacaroonHex and MacaroonDir are set, creating 353 | // NewLndServices should fail. 354 | testCfg := &LndServicesConfig{ 355 | MacaroonDir: "/testdir", 356 | CustomMacaroonHex: dummyMacStr, 357 | } 358 | 359 | _, err = NewLndServices(testCfg) 360 | require.Error(t, err, "must set only one") 361 | } 362 | -------------------------------------------------------------------------------- /log.go: -------------------------------------------------------------------------------- 1 | package lndclient 2 | 3 | import ( 4 | "github.com/btcsuite/btclog/v2" 5 | "github.com/lightningnetwork/lnd/build" 6 | ) 7 | 8 | // log is a logger that is initialized with no output filters. This 9 | // means the package will not perform any logging by default until the 10 | // caller requests it. 11 | var log btclog.Logger 12 | 13 | // The default amount of logging is none. 14 | func init() { 15 | UseLogger(build.NewSubLogger("LNDC", nil)) 16 | } 17 | 18 | // UseLogger uses a specified Logger to output package logging info. 19 | // This should be used in preference to SetLogWriter if the caller is also 20 | // using btclog. 21 | func UseLogger(logger btclog.Logger) { 22 | log = logger 23 | } 24 | -------------------------------------------------------------------------------- /macaroon_pouch.go: -------------------------------------------------------------------------------- 1 | package lndclient 2 | 3 | import ( 4 | "context" 5 | "encoding/hex" 6 | "os" 7 | "path/filepath" 8 | 9 | "google.golang.org/grpc/metadata" 10 | ) 11 | 12 | // LnrpcServiceMac is the name of a macaroon that can be used to authenticate 13 | // with a specific lnrpc service. 14 | type LnrpcServiceMac string 15 | 16 | const ( 17 | AdminServiceMac LnrpcServiceMac = "admin.macaroon" 18 | InvoiceServiceMac LnrpcServiceMac = "invoices.macaroon" 19 | ChainNotifierServiceMac LnrpcServiceMac = "chainnotifier.macaroon" 20 | WalletKitServiceMac LnrpcServiceMac = "walletkit.macaroon" 21 | RouterServiceMac LnrpcServiceMac = "router.macaroon" 22 | SignerServiceMac LnrpcServiceMac = "signer.macaroon" 23 | ReadOnlyServiceMac LnrpcServiceMac = "readonly.macaroon" 24 | ) 25 | 26 | var ( 27 | // macaroonServices is the default list of macaroon file names 28 | // that lndclient will attempt to load if a macaroon directory is given 29 | // instead of a single custom macaroon. 30 | macaroonServices = []LnrpcServiceMac{ 31 | InvoiceServiceMac, 32 | ChainNotifierServiceMac, 33 | SignerServiceMac, 34 | WalletKitServiceMac, 35 | RouterServiceMac, 36 | AdminServiceMac, 37 | ReadOnlyServiceMac, 38 | } 39 | ) 40 | 41 | // loadMacaroon tries to load a macaroon file either from the default macaroon 42 | // dir and the default filename or, if specified, from the custom macaroon path 43 | // that overwrites the former two parameters. 44 | func loadMacaroon(defaultMacDir, defaultMacFileName, 45 | customMacPath string) (serializedMacaroon, error) { 46 | 47 | // If a custom macaroon path is set, we ignore the macaroon dir and 48 | // default filename and always just load the custom macaroon, assuming 49 | // it contains all permissions needed to use the subservers. 50 | if customMacPath != "" { 51 | return newSerializedMacaroon(customMacPath) 52 | } 53 | 54 | return newSerializedMacaroon(filepath.Join( 55 | defaultMacDir, defaultMacFileName, 56 | )) 57 | } 58 | 59 | // serializedMacaroon is a type that represents a hex-encoded macaroon. We'll 60 | // use this primarily vs the raw binary format as the gRPC metadata feature 61 | // requires that all keys and values be strings. 62 | type serializedMacaroon string 63 | 64 | // newSerializedMacaroon reads a new serializedMacaroon from that target 65 | // macaroon path. If the file can't be found, then an error is returned. 66 | func newSerializedMacaroon(macaroonPath string) (serializedMacaroon, error) { 67 | macBytes, err := os.ReadFile(macaroonPath) 68 | if err != nil { 69 | return "", err 70 | } 71 | 72 | return serializedMacaroon(hex.EncodeToString(macBytes)), nil 73 | } 74 | 75 | // WithMacaroonAuth modifies the passed context to include the macaroon KV 76 | // metadata of the target macaroon. This method can be used to add the macaroon 77 | // at call time, rather than when the connection to the gRPC server is created. 78 | func (s serializedMacaroon) WithMacaroonAuth(ctx context.Context) context.Context { 79 | return metadata.AppendToOutgoingContext(ctx, "macaroon", string(s)) 80 | } 81 | 82 | // macaroonPouch holds the set of macaroons we need to interact with lnd for 83 | // Loop. Each sub-server has its own macaroon, and for the remaining temporary 84 | // calls that directly hit lnd, we'll use the admin macaroon. 85 | type macaroonPouch map[LnrpcServiceMac]serializedMacaroon 86 | 87 | // newMacaroonPouch returns a new instance of a fully populated macaroonPouch 88 | // given the directory where all the macaroons are stored. 89 | func newMacaroonPouch(macaroonDir, customMacPath, customMacHex string) (macaroonPouch, 90 | error) { 91 | 92 | // If a custom macaroon is specified, we assume it contains all 93 | // permissions needed for the different subservers to function and we 94 | // use it for all of them. 95 | var ( 96 | mac serializedMacaroon 97 | err error 98 | ) 99 | 100 | if customMacPath != "" { 101 | mac, err = loadMacaroon("", "", customMacPath) 102 | if err != nil { 103 | return nil, err 104 | } 105 | } else if customMacHex != "" { 106 | mac = serializedMacaroon(customMacHex) 107 | } 108 | 109 | if mac != "" { 110 | return macaroonPouch{ 111 | InvoiceServiceMac: mac, 112 | ChainNotifierServiceMac: mac, 113 | SignerServiceMac: mac, 114 | WalletKitServiceMac: mac, 115 | RouterServiceMac: mac, 116 | AdminServiceMac: mac, 117 | ReadOnlyServiceMac: mac, 118 | }, nil 119 | } 120 | 121 | var ( 122 | m = make(macaroonPouch) 123 | ) 124 | 125 | for _, macName := range macaroonServices { 126 | m[macName], err = loadMacaroon( 127 | macaroonDir, string(macName), customMacPath, 128 | ) 129 | if err != nil { 130 | return nil, err 131 | } 132 | } 133 | 134 | return m, nil 135 | } 136 | -------------------------------------------------------------------------------- /macaroon_recipes.go: -------------------------------------------------------------------------------- 1 | package lndclient 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | "slices" 8 | "strings" 9 | ) 10 | 11 | var ( 12 | // supportedSubservers is a map of all RPC (sub)server names that are 13 | // supported by the lndclient library and their implementing interface 14 | // type. We use reflection to look up the methods implemented on those 15 | // interfaces to find out which permissions are needed for them. 16 | supportedSubservers = map[string]interface{}{ 17 | "lnrpc": (*LightningClient)(nil), 18 | "chainrpc": (*ChainNotifierClient)(nil), 19 | "invoicesrpc": (*InvoicesClient)(nil), 20 | "routerrpc": (*RouterClient)(nil), 21 | "signrpc": (*SignerClient)(nil), 22 | "verrpc": (*VersionerClient)(nil), 23 | "walletrpc": (*WalletKitClient)(nil), 24 | } 25 | 26 | // renames is a map of renamed RPC method names. The key is the name as 27 | // implemented in lndclient and the value is the original name of the 28 | // RPC method defined in the proto. 29 | renames = map[string]string{ 30 | "ChannelBackup": "ExportChannelBackup", 31 | "ChannelBackups": "ExportAllChannelBackups", 32 | "ConfirmedWalletBalance": "WalletBalance", 33 | "Connect": "ConnectPeer", 34 | "DecodePaymentRequest": "DecodePayReq", 35 | "ListTransactions": "GetTransactions", 36 | "PayInvoice": "SendPaymentSync", 37 | "UpdateChanPolicy": "UpdateChannelPolicy", 38 | "NetworkInfo": "GetNetworkInfo", 39 | "SubscribeGraph": "SubscribeChannelGraph", 40 | "InterceptHtlcs": "HtlcInterceptor", 41 | "ImportMissionControl": "XImportMissionControl", 42 | "EstimateFeeRate": "EstimateFee", 43 | "EstimateFeeToP2WSH": "EstimateFee", 44 | "OpenChannelStream": "OpenChannel", 45 | "ListSweepsVerbose": "ListSweeps", 46 | "MinRelayFee": "EstimateFee", 47 | "SignOutputRawKeyLocator": "SignOutputRaw", 48 | } 49 | 50 | // ignores is a list of method names on the client implementations that 51 | // we don't need to check macaroon permissions for. 52 | ignores = []string{ 53 | "RawClientWithMacAuth", 54 | } 55 | ) 56 | 57 | // MacaroonRecipe returns a list of macaroon permissions that is required to use 58 | // the full feature set of the given list of RPC package names. 59 | func MacaroonRecipe(c LightningClient, packages []string) ([]MacaroonPermission, 60 | error) { 61 | 62 | // Get the full map of RPC URIs and the required permissions from the 63 | // backing lnd instance. 64 | allPermissions, err := c.ListPermissions(context.Background()) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | uniquePermissions := make(map[string]map[string]struct{}) 70 | for _, pkg := range packages { 71 | // Get the typed pointer from our map of supported interfaces. 72 | ifacePtr, ok := supportedSubservers[pkg] 73 | if !ok { 74 | return nil, fmt.Errorf("unknown subserver %s", pkg) 75 | } 76 | 77 | // From the pointer type we can find out the interface, its name 78 | // and what methods it declares. 79 | ifaceType := reflect.TypeOf(ifacePtr).Elem() 80 | serverName := strings.ReplaceAll(ifaceType.Name(), "Client", "") 81 | for i := range ifaceType.NumMethod() { 82 | // The methods in lndclient might be called slightly 83 | // differently. Rename according to our rename mapping 84 | // table. 85 | methodName := ifaceType.Method(i).Name 86 | rename, ok := renames[methodName] 87 | if ok { 88 | methodName = rename 89 | } 90 | 91 | if slices.Contains(ignores, methodName) { 92 | continue 93 | } 94 | 95 | // The full RPC URI is /package.Service/MethodName. 96 | rpcURI := fmt.Sprintf( 97 | "/%s.%s/%s", pkg, serverName, methodName, 98 | ) 99 | 100 | requiredPermissions, ok := allPermissions[rpcURI] 101 | if !ok { 102 | return nil, fmt.Errorf("URI %s not found in "+ 103 | "permission list", rpcURI) 104 | } 105 | 106 | // Add these permissions to the map we use to 107 | // de-duplicate the values. 108 | for _, perm := range requiredPermissions { 109 | actions, ok := uniquePermissions[perm.Entity] 110 | if !ok { 111 | actions = make(map[string]struct{}) 112 | uniquePermissions[perm.Entity] = actions 113 | } 114 | actions[perm.Action] = struct{}{} 115 | } 116 | } 117 | } 118 | 119 | // Turn the de-duplicated map back into a slice of permission entries. 120 | var requiredPermissions []MacaroonPermission 121 | for entity, actions := range uniquePermissions { 122 | for action := range actions { 123 | requiredPermissions = append( 124 | requiredPermissions, MacaroonPermission{ 125 | Entity: entity, 126 | Action: action, 127 | }, 128 | ) 129 | } 130 | } 131 | return requiredPermissions, nil 132 | } 133 | -------------------------------------------------------------------------------- /macaroon_recipes_test.go: -------------------------------------------------------------------------------- 1 | package lndclient_test 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "os" 7 | "testing" 8 | 9 | "github.com/lightninglabs/lndclient" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | var ( 14 | expectedPermissions = map[string]int{ 15 | "lnrpc": 13, 16 | "chainrpc": 1, 17 | "invoicesrpc": 2, 18 | "routerrpc": 2, 19 | "signrpc": 2, 20 | "verrpc": 1, 21 | "walletrpc": 3, 22 | } 23 | ) 24 | 25 | type permissionJSONData struct { 26 | Permissions map[string]struct { 27 | Permissions []struct { 28 | Entity string `json:"entity"` 29 | Action string `json:"action"` 30 | } `json:"permissions"` 31 | } `json:"method_permissions"` 32 | } 33 | 34 | type lightningMock struct { 35 | lndclient.LightningClient 36 | 37 | mockPermissions map[string][]lndclient.MacaroonPermission 38 | } 39 | 40 | func (m *lightningMock) ListPermissions( 41 | _ context.Context) (map[string][]lndclient.MacaroonPermission, error) { 42 | 43 | return m.mockPermissions, nil 44 | } 45 | 46 | // TestMacaroonRecipe makes sure the macaroon recipe for all supported packages 47 | // can be generated. 48 | func TestMacaroonRecipe(t *testing.T) { 49 | // Load our static permissions exported from lnd by calling 50 | // `lncli listpermissions > permissions.json`. 51 | content, err := os.ReadFile("testdata/permissions.json") 52 | require.NoError(t, err) 53 | 54 | data := &permissionJSONData{} 55 | err = json.Unmarshal(content, data) 56 | require.NoError(t, err) 57 | 58 | mockPermissions := make(map[string][]lndclient.MacaroonPermission) 59 | for uri, perms := range data.Permissions { 60 | mockPermissions[uri] = make( 61 | []lndclient.MacaroonPermission, len(perms.Permissions), 62 | ) 63 | for idx, perm := range perms.Permissions { 64 | mockPermissions[uri][idx] = lndclient.MacaroonPermission{ 65 | Entity: perm.Entity, 66 | Action: perm.Action, 67 | } 68 | } 69 | } 70 | clientMock := &lightningMock{ 71 | mockPermissions: mockPermissions, 72 | } 73 | 74 | // Run the test for all supported RPC packages. 75 | for pkg, numPermissions := range expectedPermissions { 76 | t.Run(pkg, func(t *testing.T) { 77 | t.Parallel() 78 | 79 | requiredPermissions, err := lndclient.MacaroonRecipe( 80 | clientMock, []string{pkg}, 81 | ) 82 | require.NoError(t, err) 83 | require.Len(t, requiredPermissions, numPermissions) 84 | }) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /macaroon_service.go: -------------------------------------------------------------------------------- 1 | package lndclient 2 | 3 | import ( 4 | "context" 5 | "encoding/hex" 6 | "errors" 7 | "fmt" 8 | "os" 9 | "time" 10 | 11 | "github.com/btcsuite/btcd/btcec/v2" 12 | "github.com/lightningnetwork/lnd/keychain" 13 | "github.com/lightningnetwork/lnd/kvdb" 14 | "github.com/lightningnetwork/lnd/lnrpc" 15 | "github.com/lightningnetwork/lnd/macaroons" 16 | "github.com/lightningnetwork/lnd/rpcperms" 17 | "google.golang.org/grpc" 18 | "gopkg.in/macaroon-bakery.v2/bakery" 19 | "gopkg.in/macaroon-bakery.v2/bakery/checkers" 20 | "gopkg.in/macaroon.v2" 21 | ) 22 | 23 | const ( 24 | // defaultMacaroonTimeout is the default timeout to be used for general 25 | // macaroon operation, such as for the macaroon db connection, and for 26 | // creating a context with a timeout when validating macaroons. 27 | defaultMacaroonTimeout = 5 * time.Second 28 | ) 29 | 30 | var ( 31 | // sharedKeyNUMSBytes holds the bytes representing the compressed 32 | // byte encoding of SharedKeyNUMS. It was generated via a 33 | // try-and-increment approach using the phrase "Shared Secret" with 34 | // SHA2-256. The code for the try-and-increment approach can be seen 35 | // here: https://github.com/lightninglabs/lightning-node-connect/tree/master/mailbox/numsgen 36 | sharedKeyNUMSBytes, _ = hex.DecodeString( 37 | "0215b5a3e0ef58b101431e1e513dd017d1018b420bd2e89dcd71f45c031f00469e", 38 | ) 39 | 40 | // SharedKeyNUMS is the public key point that can be use for when we 41 | // are deriving a shared secret key with LND. 42 | SharedKeyNUMS, _ = btcec.ParsePubKey(sharedKeyNUMSBytes) 43 | 44 | // SharedKeyLocator is a key locator that can be used for deriving a 45 | // shared secret key with LND. 46 | SharedKeyLocator = &keychain.KeyLocator{ 47 | Family: 21, 48 | Index: 0, 49 | } 50 | ) 51 | 52 | // MacaroonService handles the creatation and unlocking of a macaroon DB file. 53 | // It is also a wrapper for a macaroon.Service and uses this to create a 54 | // default macaroon for the caller if stateless mode has not been specified. 55 | type MacaroonService struct { 56 | cfg *MacaroonServiceConfig 57 | 58 | *macaroons.Service 59 | } 60 | 61 | // MacaroonServiceConfig holds configuration values used by the MacaroonService. 62 | type MacaroonServiceConfig struct { 63 | // RootKeyStorage is an implementation of the main 64 | // bakery.RootKeyStorage interface. This implementation may also 65 | // concurrenlty implement the larger macaroons.ExtendedRootKeyStore 66 | // interface as well. 67 | RootKeyStore bakery.RootKeyStore 68 | 69 | // MacaroonLocation is the value used for a macaroons' "Location" field. 70 | MacaroonLocation string 71 | 72 | // MacaroonPath is the path to where macaroons should be stored. 73 | MacaroonPath string 74 | 75 | // StatelessInit should be set to true if no default macaroons should 76 | // be created and stored on disk. 77 | StatelessInit bool 78 | 79 | // Checkers are used to add extra validation checks on macaroon. 80 | Checkers []macaroons.Checker 81 | 82 | // RequiredPerms defines all method paths and the permissions required 83 | // when accessing those paths. 84 | RequiredPerms map[string][]bakery.Op 85 | 86 | // Caveats is a list of caveats that will be added to the default 87 | // macaroons. 88 | Caveats []checkers.Caveat 89 | 90 | // DBPassword is the password that will be used to encrypt the macaroon 91 | // db. If DBPassword is not set, then LndClient, EphemeralKey and 92 | // KeyLocator must be set instead. 93 | DBPassword []byte 94 | 95 | // LndClient is an LND client that can be used for any lnd queries. 96 | // This only needs to be set if DBPassword is not set. 97 | LndClient *LndServices 98 | 99 | // RPCTimeout is the time after which an RPC call will be canceled if 100 | // it has not yet completed. 101 | RPCTimeout time.Duration 102 | 103 | // EphemeralKey is a key that will be used to derive a shared secret 104 | // with LND. This only needs to be set if DBPassword is not set. 105 | EphemeralKey *btcec.PublicKey 106 | 107 | // KeyLocator is the locator used to derive a shared secret with LND. 108 | // This only needs to be set if DBPassword is not set. 109 | KeyLocator *keychain.KeyLocator 110 | } 111 | 112 | // NewMacaroonService checks the config values passed in and creates a 113 | // MacaroonService object accordingly. 114 | func NewMacaroonService(cfg *MacaroonServiceConfig) (*MacaroonService, error) { 115 | // Validate config. 116 | if cfg.MacaroonLocation == "" { 117 | return nil, errors.New("no macaroon location provided") 118 | } 119 | 120 | if cfg.RPCTimeout == 0 { 121 | cfg.RPCTimeout = defaultRPCTimeout 122 | } else if cfg.RPCTimeout < 0 { 123 | return nil, errors.New("can't have a negative rpc timeout") 124 | } 125 | 126 | if !cfg.StatelessInit && cfg.MacaroonPath == "" { 127 | return nil, errors.New("a macaroon path must be given if we " + 128 | "are not in stateless mode") 129 | } 130 | 131 | if len(cfg.RequiredPerms) == 0 { 132 | return nil, errors.New("the required permissions must be set " + 133 | "and contain elements") 134 | } 135 | 136 | ms := MacaroonService{ 137 | cfg: cfg, 138 | } 139 | 140 | if len(cfg.DBPassword) != 0 { 141 | return &ms, nil 142 | } 143 | 144 | _, extendedKeyStore := ms.cfg.RootKeyStore.(macaroons.ExtendedRootKeyStore) 145 | if !extendedKeyStore { 146 | return &ms, nil 147 | } 148 | 149 | if cfg.LndClient == nil || cfg.EphemeralKey == nil || 150 | cfg.KeyLocator == nil { 151 | 152 | return nil, errors.New("must provide an LndClient, ephemeral " + 153 | "key and key locator if no DBPassword is provided " + 154 | "so that a shared key can be derived with LND") 155 | } 156 | 157 | return &ms, nil 158 | } 159 | 160 | // Start starts the macaroon validation service, creates or unlocks the 161 | // macaroon database and, if we are not in stateless mode, creates the default 162 | // macaroon if it doesn't exist yet or regenerates the macaroon if the required 163 | // permissions have changed. 164 | func (ms *MacaroonService) Start() error { 165 | // Create the macaroon authentication/authorization service. 166 | service, err := macaroons.NewService( 167 | ms.cfg.RootKeyStore, ms.cfg.MacaroonLocation, ms.cfg.StatelessInit, 168 | ms.cfg.Checkers..., 169 | ) 170 | if err != nil { 171 | return fmt.Errorf("unable to set up macaroon service: %v", err) 172 | } 173 | ms.Service = service 174 | 175 | _, extendedKeyStore := ms.cfg.RootKeyStore.(macaroons.ExtendedRootKeyStore) 176 | switch { 177 | // The passed root key store doesn't use the extended interface, so we 178 | // can skip everything below. 179 | case !extendedKeyStore: 180 | break 181 | 182 | case len(ms.cfg.DBPassword) != 0: 183 | // If a non-empty DB password was provided, then use this 184 | // directly to try and unlock the db. 185 | err := ms.CreateUnlock(&ms.cfg.DBPassword) 186 | if err != nil { 187 | return fmt.Errorf("unable to unlock macaroon DB: %v", 188 | err) 189 | } 190 | 191 | default: 192 | // If an empty DB password was provided, we want to establish a 193 | // shared secret with LND which we will use as our DB password. 194 | ctx, cancel := context.WithTimeout( 195 | context.Background(), ms.cfg.RPCTimeout, 196 | ) 197 | defer cancel() 198 | 199 | sharedKey, err := ms.cfg.LndClient.Signer.DeriveSharedKey( 200 | ctx, ms.cfg.EphemeralKey, ms.cfg.KeyLocator, 201 | ) 202 | if err != nil { 203 | return fmt.Errorf("unable to derive a shared "+ 204 | "secret with LND: %v", err) 205 | } 206 | 207 | // Try to unlock the macaroon store with the shared key. 208 | dbPassword := sharedKey[:] 209 | err = ms.CreateUnlock(&dbPassword) 210 | if err == nil { 211 | // If the db was successfully unlocked, we can continue. 212 | break 213 | } 214 | 215 | log.Infof("Macaroon DB could not be unlocked with the " + 216 | "derived shared key. Attempting to unlock with " + 217 | "empty password instead") 218 | 219 | // Otherwise, we will attempt to unlock the db with an empty 220 | // password. If this succeeds, we will re-encrypt it with 221 | // the shared key. 222 | dbPassword = []byte{} 223 | err = ms.CreateUnlock(&dbPassword) 224 | if err != nil { 225 | return fmt.Errorf("unable to unlock macaroon DB: %v", 226 | err) 227 | } 228 | 229 | log.Infof("Re-encrypting macaroon DB with derived shared key") 230 | 231 | // Attempt to now re-encrypt the DB with the shared key. 232 | err = ms.ChangePassword(dbPassword, sharedKey[:]) 233 | if err != nil { 234 | return fmt.Errorf("unable to change the macaroon "+ 235 | "DB password: %v", err) 236 | } 237 | } 238 | 239 | // There are situations in which we don't want a macaroon to be created 240 | // on disk (for example when running inside LiT stateless integrated 241 | // mode). 242 | if ms.cfg.StatelessInit { 243 | return nil 244 | } 245 | 246 | // If we are not in stateless mode and a macaroon file does exist, we 247 | // check that the macaroon matches the required permissions. If not, we 248 | // will delete the macaroon and create a new one. 249 | if lnrpc.FileExists(ms.cfg.MacaroonPath) { 250 | matches, err := ms.macaroonMatchesPermissions() 251 | if err != nil { 252 | log.Warnf("An error occurred when attempting to match "+ 253 | "the previous macaroon's permissions with the "+ 254 | "current required permissions. This may occur "+ 255 | "if the previous macaroon file is corrupted. "+ 256 | "The path to the file attempted to be used as "+ 257 | "the previous macaroon is: %s. If that file "+ 258 | "is the correct macaroon file and this error "+ 259 | "happens repeatedly on startup, please remove "+ 260 | "the macaroon file manually and restart "+ 261 | "once again to generate a new macaroon.", 262 | ms.cfg.MacaroonPath) 263 | 264 | return fmt.Errorf("unable to match the previous "+ 265 | "macaroon's permissions with the required "+ 266 | "permissions: %v", err) 267 | } 268 | 269 | // In case the old macaroon matches the required permissions, 270 | // we don't need to create a new macaroon. 271 | if matches { 272 | return nil 273 | } 274 | 275 | // Else if the required permissions have been updated, we delete 276 | // the old macaroon and create a new one. 277 | log.Infof("Macaroon at %s does not have all required "+ 278 | "permissions. Deleting it and creating a new "+ 279 | "one", ms.cfg.MacaroonPath) 280 | } 281 | 282 | // We don't offer the ability to rotate macaroon root keys yet, so just 283 | // use the default one since the service expects some value to be set. 284 | idCtx := macaroons.ContextWithRootKeyID( 285 | context.Background(), macaroons.DefaultRootKeyID, 286 | ) 287 | 288 | // We only generate one default macaroon that contains all existing 289 | // permissions (equivalent to the admin.macaroon in lnd). Custom 290 | // macaroons can be created through the bakery RPC. 291 | mac, err := ms.Oven.NewMacaroon( 292 | idCtx, bakery.LatestVersion, ms.cfg.Caveats, 293 | extractPerms(ms.cfg.RequiredPerms)..., 294 | ) 295 | if err != nil { 296 | return err 297 | } 298 | 299 | macBytes, err := mac.M().MarshalBinary() 300 | if err != nil { 301 | return err 302 | } 303 | 304 | err = os.WriteFile(ms.cfg.MacaroonPath, macBytes, 0644) 305 | 306 | return err 307 | } 308 | 309 | // Stop cleans up the MacaroonService. 310 | func (ms *MacaroonService) Stop() error { 311 | var shutdownErr error 312 | if err := ms.Close(); err != nil { 313 | log.Errorf("Error closing macaroon service: %v", err) 314 | shutdownErr = err 315 | } 316 | 317 | rks := ms.cfg.RootKeyStore 318 | if eRKS, ok := rks.(macaroons.ExtendedRootKeyStore); ok { 319 | if err := eRKS.Close(); err != nil { 320 | log.Errorf("Error closing macaroon DB: %v", err) 321 | shutdownErr = err 322 | } 323 | } 324 | 325 | return shutdownErr 326 | } 327 | 328 | // extractPerms creates a deduped list of all the perms in a required perms map. 329 | func extractPerms(requiredPerms map[string][]bakery.Op) []bakery.Op { 330 | entityActionPairs := make(map[string]map[string]struct{}) 331 | 332 | for _, perms := range requiredPerms { 333 | for _, p := range perms { 334 | if _, ok := entityActionPairs[p.Entity]; !ok { 335 | entityActionPairs[p.Entity] = make( 336 | map[string]struct{}, 337 | ) 338 | } 339 | 340 | entityActionPairs[p.Entity][p.Action] = struct{}{} 341 | } 342 | } 343 | 344 | // Dedup the permissions. 345 | perms := make([]bakery.Op, 0) 346 | for entity, actions := range entityActionPairs { 347 | for action := range actions { 348 | perms = append(perms, bakery.Op{ 349 | Entity: entity, 350 | Action: action, 351 | }) 352 | } 353 | } 354 | 355 | return perms 356 | } 357 | 358 | // Interceptors creates gRPC server options with the macaroon security 359 | // interceptors. 360 | func (ms *MacaroonService) Interceptors() (grpc.UnaryServerInterceptor, 361 | grpc.StreamServerInterceptor, error) { 362 | 363 | interceptor := rpcperms.NewInterceptorChain(log, false, nil) 364 | err := interceptor.Start() 365 | if err != nil { 366 | return nil, nil, err 367 | } 368 | 369 | interceptor.SetWalletUnlocked() 370 | interceptor.AddMacaroonService(ms.Service) 371 | for method, permissions := range ms.cfg.RequiredPerms { 372 | err := interceptor.AddPermission(method, permissions) 373 | if err != nil { 374 | return nil, nil, err 375 | } 376 | } 377 | 378 | unaryInterceptor := interceptor.MacaroonUnaryServerInterceptor() 379 | streamInterceptor := interceptor.MacaroonStreamServerInterceptor() 380 | return unaryInterceptor, streamInterceptor, nil 381 | } 382 | 383 | // NewBoltMacaroonStore returns a new bakery.RootKeyStore, backed by a bolt DB 384 | // instance at the specified location. 385 | func NewBoltMacaroonStore(dbPath, dbFileName string, 386 | dbTimeout time.Duration) (bakery.RootKeyStore, kvdb.Backend, error) { 387 | 388 | db, err := kvdb.GetBoltBackend(&kvdb.BoltBackendConfig{ 389 | DBPath: dbPath, 390 | DBFileName: dbFileName, 391 | DBTimeout: dbTimeout, 392 | }) 393 | if err != nil { 394 | return nil, nil, fmt.Errorf("unable to open macaroon "+ 395 | "db: %w", err) 396 | } 397 | 398 | rks, err := macaroons.NewRootKeyStorage(db) 399 | if err != nil { 400 | return nil, nil, fmt.Errorf("unable to open init macaroon "+ 401 | "db: %w", err) 402 | } 403 | 404 | return rks, db, nil 405 | } 406 | 407 | // macaroonMatchesPermissions checks if the macaroon at the cfg.MacaroonPath 408 | // matches the required permissions. It returns true if the macaroon matches the 409 | // required permissions. 410 | func (ms *MacaroonService) macaroonMatchesPermissions() (bool, error) { 411 | macBytes, err := os.ReadFile(ms.cfg.MacaroonPath) 412 | if err != nil { 413 | return false, fmt.Errorf("unable to read macaroon path: %v", 414 | err) 415 | } 416 | 417 | // Make sure it actually is a macaroon by parsing it. 418 | oldMac := &macaroon.Macaroon{} 419 | if err := oldMac.UnmarshalBinary(macBytes); err != nil { 420 | return false, fmt.Errorf("unable to decode macaroon: %v", err) 421 | } 422 | 423 | var ( 424 | authChecker = ms.Checker.Auth(macaroon.Slice{oldMac}) 425 | requiredPerms = extractPerms(ms.cfg.RequiredPerms) 426 | ) 427 | 428 | ctx, cancel := context.WithTimeout( 429 | context.Background(), defaultMacaroonTimeout, 430 | ) 431 | defer cancel() 432 | 433 | _, err = authChecker.Allow(ctx, requiredPerms...) 434 | if err != nil { 435 | // If an error is returned here, it's most likely because the 436 | // old macaroon doesn't match the required permissions. We 437 | // therefore return false but not the error as this is expected 438 | // behavior. 439 | return false, nil 440 | } 441 | 442 | // If the number of ops in the allowed info is not the same as the 443 | // number of required permissions, i.e. there are fewer required 444 | // permissions than allowed ops, then the required permissions have been 445 | // modified to require fewer permissions than the old macaroon has. We 446 | // therefore need to regenerate the macaroon. 447 | allowedInfo, err := authChecker.Allowed(ctx) 448 | if err != nil { 449 | return false, err 450 | } 451 | if len(allowedInfo.OpIndexes) != len(requiredPerms) { 452 | return false, nil 453 | } 454 | 455 | // The old macaroon matches the required permissions. 456 | return true, nil 457 | } 458 | -------------------------------------------------------------------------------- /macaroon_service_test.go: -------------------------------------------------------------------------------- 1 | package lndclient 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "testing" 9 | 10 | "github.com/btcsuite/btcd/btcec/v2" 11 | "github.com/lightningnetwork/lnd/keychain" 12 | "github.com/lightningnetwork/lnd/macaroons" 13 | "github.com/stretchr/testify/require" 14 | "gopkg.in/macaroon-bakery.v2/bakery" 15 | "gopkg.in/macaroon.v2" 16 | ) 17 | 18 | // TestMacaroonServiceMigration tests that a client that was using a macaroon 19 | // service encrypted with an empty passphrase can successfully migrate to 20 | // using a shared key passphrase. 21 | func TestMacaroonServiceMigration(t *testing.T) { 22 | // Create a temporary directory where we can store the macaroon db and 23 | // the macaroon we are about to create. 24 | tempDirPath := t.TempDir() 25 | 26 | rks, backend, err := NewBoltMacaroonStore( 27 | tempDirPath, "macaroons.db", defaultMacaroonTimeout, 28 | ) 29 | require.NoError(t, err) 30 | defer func() { 31 | require.NoError(t, backend.Close()) 32 | }() 33 | 34 | mockPerms := map[string][]bakery.Op{ 35 | "/uri.Test/WriteTest": { 36 | bakery.Op{Entity: "entity", Action: "write"}, 37 | }, 38 | } 39 | 40 | // The initial config we will use has an empty DB password. 41 | cfg := &MacaroonServiceConfig{ 42 | MacaroonLocation: "testLocation", 43 | MacaroonPath: filepath.Join(tempDirPath, "test.macaroon"), 44 | DBPassword: []byte{}, 45 | RootKeyStore: rks, 46 | RequiredPerms: mockPerms, 47 | } 48 | 49 | // Create a new macaroon service with an empty password. 50 | testService, err := createTestService(cfg) 51 | require.NoError(t, err) 52 | defer func() { 53 | require.NoError(t, testService.stop()) 54 | }() 55 | 56 | err = testService.CreateUnlock(&cfg.DBPassword) 57 | require.NoError(t, err) 58 | 59 | // We generate a new root key. This is required for the call the 60 | // ChangePassword to succeed. 61 | err = testService.GenerateNewRootKey() 62 | require.NoError(t, err) 63 | 64 | // Close the test db. 65 | err = testService.stop() 66 | require.NoError(t, err) 67 | 68 | // Now we will restart the DB but using the new MacaroonService Start 69 | // function which will attempt to upgrade our db to be encrypted with 70 | // a shared secret with LND if we give an empty DB password. 71 | cfg.EphemeralKey = SharedKeyNUMS 72 | cfg.KeyLocator = SharedKeyLocator 73 | sharedSecret := []byte("shared secret") 74 | cfg.LndClient = &LndServices{Signer: &mockSignerClient{ 75 | sharedKey: sharedSecret, 76 | }} 77 | 78 | ms, err := NewMacaroonService(cfg) 79 | require.NoError(t, err) 80 | 81 | // We now start the service. This will attempt to unlock the db using 82 | // the shared secret with LND. This will initially fail and so 83 | // decryption with an empty passphrase will be attempted. If this 84 | // succeeds, then the db will be re-encrypted with the new shared 85 | // secret. 86 | require.NoError(t, ms.Start()) 87 | require.NoError(t, ms.Stop()) 88 | 89 | // To test that the db has been successfully re-encrypted with the new 90 | // key, we remove the connection to lnd and use the shared secret 91 | // directly as the new DB password. 92 | cfg.EphemeralKey = nil 93 | cfg.KeyLocator = nil 94 | cfg.LndClient = nil 95 | cfg.DBPassword = sharedSecret 96 | ms, err = NewMacaroonService(cfg) 97 | require.NoError(t, err) 98 | 99 | require.NoError(t, ms.Start()) 100 | require.NoError(t, ms.Stop()) 101 | } 102 | 103 | // TestMacaroonGeneration tests that the macaroon service generates macaroons 104 | // as expected. A macaroon should not be regenerated if the required permissions 105 | // haven't changed, and should be regenerated if the required permissions have 106 | // changed. 107 | func TestMacaroonGeneration(t *testing.T) { 108 | // Create a temporary directory where we can store the macaroon db and 109 | // the macaroon we are about to create. 110 | tempDirPath := t.TempDir() 111 | macaroonPath := filepath.Join(tempDirPath, "test.macaroon") 112 | 113 | // Set the macaroon store, and get the root key store. 114 | rks, backend, err := NewBoltMacaroonStore( 115 | tempDirPath, "macaroons.db", defaultMacaroonTimeout, 116 | ) 117 | require.NoError(t, err) 118 | t.Cleanup(func() { 119 | require.NoError(t, backend.Close()) 120 | }) 121 | 122 | // Generate mock permissions. 123 | firstEntityWrite := bakery.Op{Entity: "first_entity", Action: "write"} 124 | firstEntityRead := bakery.Op{Entity: "first_entity", Action: "read"} 125 | secondEntityRead := bakery.Op{Entity: "second_entity", Action: "read"} 126 | 127 | // We'll initially use only part of the mock permissions, but we'll 128 | // modify the required permissions later for the different tests. 129 | mockPerms := map[string][]bakery.Op{ 130 | "/uri1.Test/WriteTest": {firstEntityWrite}, 131 | "/uri1.Test/ReadTest": {firstEntityRead}, 132 | } 133 | 134 | // Create the initial config. We set a mock DB password, as we either 135 | // need to set a DB password or provide an LndClient, ephemeral key and 136 | // key locator so that a shared key can be derived with LND, when 137 | // creating the macaroon service. 138 | cfg := &MacaroonServiceConfig{ 139 | MacaroonLocation: tempDirPath, 140 | DBPassword: []byte{1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, 141 | RootKeyStore: rks, 142 | MacaroonPath: macaroonPath, 143 | RequiredPerms: mockPerms, 144 | } 145 | 146 | // Create the macaroon service we will use for creating macaroons. 147 | ms, err := NewMacaroonService(cfg) 148 | require.NoError(t, err) 149 | 150 | // Firstly, we test that will fail to create a macaroon if we set 151 | // the MacaroonPath to an existing file which isn't a macaroon. 152 | cfg.MacaroonPath = filepath.Join(tempDirPath, "macaroons.db") 153 | 154 | require.Error(t, ms.Start()) 155 | require.NoError(t, ms.Stop()) 156 | 157 | // Reset the macaroon path to the correct path. 158 | cfg.MacaroonPath = macaroonPath 159 | 160 | // We'll now test that the macaroon service generates and regenerates 161 | // macaroons as expected. We'll do this in several steps: 162 | 163 | // 1. With the 2 required permissions in the mockPerms map, we ensure 164 | // that the macaroon service generates a macaroon with the correct 165 | // permissions. 166 | require.NoError(t, ms.Start()) 167 | 168 | // Ensure that the created macaroon has the correct permissions. 169 | mac1, mac1Bytes := extractMacaroon(t, cfg.MacaroonPath) 170 | assertMacaroonPerms(t, mac1, mockPerms, ms) 171 | 172 | require.NoError(t, ms.Stop()) 173 | 174 | // 2. Secondly, we use the same permissions as last time, and ensure 175 | // that the macaroon hasn't been regenerated. 176 | require.NoError(t, ms.Start()) 177 | _, mac2Bytes := extractMacaroon(t, cfg.MacaroonPath) 178 | 179 | // We then ensure that the macaroon bytes from the mac1 is the same as 180 | // mac2. This ensures that the macaroon service didn't regenerate the 181 | // macaroon, as if it did, the macaroon bytes would be different as 182 | // another nonce would have been used when generating mac2, resulting 183 | // in different macaroon bytes (the signature for the macaroon would 184 | // also differ). 185 | require.Equal(t, mac1Bytes, mac2Bytes) 186 | require.NoError(t, ms.Stop()) 187 | 188 | // 3. Thirdly, we add a permission to the mockPerms map that is for a 189 | // different URI, but that requires an entity and action we already have 190 | // in the previous macaroon. We then ensure that the macaroon service 191 | // doesn't regenerate the macaroon, as it's not needed. 192 | mockPerms["/uri2.Test/WriteTest"] = []bakery.Op{firstEntityWrite} 193 | cfg.RequiredPerms = mockPerms 194 | 195 | require.NoError(t, ms.Start()) 196 | 197 | _, mac3Bytes := extractMacaroon(t, cfg.MacaroonPath) 198 | 199 | require.Equal(t, mac1Bytes, mac3Bytes) 200 | require.NoError(t, ms.Stop()) 201 | 202 | // 4. Fourthly, we add a permission to the mockPerms map that requires a 203 | // new entity and action which the previous macaroon doesn't contain. We 204 | // then ensure that the macaroon service regenerates the macaroon, as 205 | // it's needed. 206 | mockPerms["/uri3.Test/ReadTest"] = []bakery.Op{secondEntityRead} 207 | cfg.RequiredPerms = mockPerms 208 | 209 | require.NoError(t, ms.Start()) 210 | mac4, mac4Bytes := extractMacaroon(t, cfg.MacaroonPath) 211 | assertMacaroonPerms(t, mac4, mockPerms, ms) 212 | 213 | // We're already sure that the macaroon service regenerated the macaroon 214 | // as assertMacaroonPerms would have failed if it didn't, but we 215 | // also ensure that the macaroon bytes are different from the previous 216 | // macaroon just to clarify that it was regenerated. 217 | require.NotEqual(t, mac1Bytes, mac4Bytes) 218 | require.NoError(t, ms.Stop()) 219 | 220 | // 5. Fifthly, we remove a required entity. We then ensure that the 221 | // macaroon service also regenerates the macaroon, even though the 222 | // previous macaroon already contains the required permissions, as we'd 223 | // like to ensure that we don't keep unnecessary permissions no longer 224 | // required in the macaroon. 225 | delete(mockPerms, "/uri3.Test/ReadTest") 226 | cfg.RequiredPerms = mockPerms 227 | 228 | require.NoError(t, ms.Start()) 229 | mac5, mac5Bytes := extractMacaroon(t, cfg.MacaroonPath) 230 | assertMacaroonPerms(t, mac5, mockPerms, ms) 231 | 232 | require.NotEqual(t, mac4Bytes, mac5Bytes) 233 | require.NoError(t, ms.Stop()) 234 | 235 | // 6. Lastly, we keep the same permissions, but change the macaroon 236 | // path. We then ensure that the macaroon service crates a new macaroon. 237 | cfg.MacaroonPath = filepath.Join(tempDirPath, "test2.macaroon") 238 | 239 | require.NoError(t, ms.Start()) 240 | mac6, mac6Bytes := extractMacaroon(t, cfg.MacaroonPath) 241 | assertMacaroonPerms(t, mac6, mockPerms, ms) 242 | 243 | require.NotEqual(t, mac5Bytes, mac6Bytes) 244 | require.NoError(t, ms.Stop()) 245 | } 246 | 247 | type testMacaroonService struct { 248 | *macaroons.Service 249 | rks bakery.RootKeyStore 250 | } 251 | 252 | func createTestService(cfg *MacaroonServiceConfig) (*testMacaroonService, 253 | error) { 254 | 255 | // Create the macaroon authentication/authorization service. 256 | service, err := macaroons.NewService( 257 | cfg.RootKeyStore, cfg.MacaroonLocation, cfg.StatelessInit, 258 | cfg.Checkers..., 259 | ) 260 | if err != nil { 261 | return nil, fmt.Errorf("unable to set up macaroon "+ 262 | "service: %v", err) 263 | } 264 | 265 | return &testMacaroonService{ 266 | Service: service, 267 | rks: cfg.RootKeyStore, 268 | }, nil 269 | } 270 | 271 | func (s *testMacaroonService) stop() error { 272 | var returnErr error 273 | if eRKS, ok := s.rks.(macaroons.ExtendedRootKeyStore); ok { 274 | if err := eRKS.Close(); err != nil { 275 | returnErr = err 276 | } 277 | } 278 | 279 | if err := s.Close(); err != nil { 280 | returnErr = err 281 | } 282 | 283 | return returnErr 284 | } 285 | 286 | type mockSignerClient struct { 287 | sharedKey []byte 288 | 289 | SignerClient 290 | } 291 | 292 | func (m *mockSignerClient) DeriveSharedKey(_ context.Context, 293 | _ *btcec.PublicKey, _ *keychain.KeyLocator) ([32]byte, error) { 294 | 295 | var res [32]byte 296 | copy(res[:], m.sharedKey) 297 | 298 | return res, nil 299 | } 300 | 301 | func extractMacaroon(t *testing.T, 302 | macaroonPath string) (*macaroon.Macaroon, []byte) { 303 | 304 | macBytes, err := os.ReadFile(macaroonPath) 305 | require.NoError(t, err) 306 | 307 | mac := &macaroon.Macaroon{} 308 | err = mac.UnmarshalBinary(macBytes) 309 | require.NoError(t, err) 310 | 311 | return mac, macBytes 312 | } 313 | 314 | func assertMacaroonPerms(t *testing.T, mac *macaroon.Macaroon, 315 | expectedPermissions map[string][]bakery.Op, ms *MacaroonService) { 316 | 317 | var ( 318 | authChecker = ms.Checker.Auth(macaroon.Slice{mac}) 319 | expectedPerms = extractPerms(expectedPermissions) 320 | ) 321 | 322 | allowedInfo, err := authChecker.Allowed(context.Background()) 323 | require.NoError(t, err) 324 | 325 | for _, expectedPerm := range expectedPerms { 326 | require.Contains(t, allowedInfo.OpIndexes, expectedPerm) 327 | } 328 | require.Len(t, allowedInfo.OpIndexes, len(expectedPerms)) 329 | } 330 | -------------------------------------------------------------------------------- /network.go: -------------------------------------------------------------------------------- 1 | package lndclient 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/btcsuite/btcd/chaincfg" 7 | ) 8 | 9 | // Network defines the chain that we operate on. 10 | type Network string 11 | 12 | const ( 13 | // NetworkMainnet is bitcoin mainnet. 14 | NetworkMainnet Network = "mainnet" 15 | 16 | // NetworkTestnet is bitcoin testnet. 17 | NetworkTestnet Network = "testnet" 18 | 19 | // NetworkTestnet4 is bitcoin testnet version 4. 20 | NetworkTestnet4 Network = "testnet4" 21 | 22 | // NetworkRegtest is bitcoin regtest. 23 | NetworkRegtest Network = "regtest" 24 | 25 | // NetworkSimnet is bitcoin simnet. 26 | NetworkSimnet Network = "simnet" 27 | 28 | // NetworkSignet is bitcoin signet. 29 | NetworkSignet Network = "signet" 30 | ) 31 | 32 | // ChainParams returns chain parameters based on a network name. 33 | func (n Network) ChainParams() (*chaincfg.Params, error) { 34 | switch n { 35 | case NetworkMainnet: 36 | return &chaincfg.MainNetParams, nil 37 | 38 | case NetworkTestnet: 39 | return &chaincfg.TestNet3Params, nil 40 | 41 | case NetworkTestnet4: 42 | return &chaincfg.TestNet4Params, nil 43 | 44 | case NetworkRegtest: 45 | return &chaincfg.RegressionNetParams, nil 46 | 47 | case NetworkSimnet: 48 | return &chaincfg.SimNetParams, nil 49 | 50 | case NetworkSignet: 51 | return &chaincfg.SigNetParams, nil 52 | 53 | default: 54 | return nil, errors.New("unknown network") 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /signer_client.go: -------------------------------------------------------------------------------- 1 | package lndclient 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/btcsuite/btcd/btcec/v2" 9 | "github.com/btcsuite/btcd/btcec/v2/schnorr" 10 | "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" 11 | "github.com/btcsuite/btcd/txscript" 12 | "github.com/btcsuite/btcd/wire" 13 | "github.com/lightningnetwork/lnd/input" 14 | "github.com/lightningnetwork/lnd/keychain" 15 | "github.com/lightningnetwork/lnd/lnrpc/signrpc" 16 | "google.golang.org/grpc" 17 | ) 18 | 19 | // SignerClient exposes sign functionality. 20 | type SignerClient interface { 21 | ServiceClient[signrpc.SignerClient] 22 | 23 | // SignOutputRaw is a method that can be used to generate a signature 24 | // for a set of inputs/outputs to a transaction. Each request specifies 25 | // details concerning how the outputs should be signed, which keys they 26 | // should be signed with, and also any optional tweaks. 27 | SignOutputRaw(ctx context.Context, tx *wire.MsgTx, 28 | signDescriptors []*SignDescriptor, 29 | prevOutputs []*wire.TxOut) ([][]byte, error) 30 | 31 | // SignOutputRawKeyLocator is a copy of the SignOutputRaw that fixes a 32 | // specific issue around how the key locator is populated in the sign 33 | // descriptor. We copy this method instead of fixing the original to 34 | // make sure we don't break any existing applications that have already 35 | // adjusted themselves to use the specific behavior of the original 36 | // SignOutputRaw method. 37 | SignOutputRawKeyLocator(ctx context.Context, tx *wire.MsgTx, 38 | signDescriptors []*SignDescriptor, 39 | prevOutputs []*wire.TxOut) ([][]byte, error) 40 | 41 | // ComputeInputScript generates the proper input script for P2WPKH 42 | // output and NP2WPKH outputs. This method only requires that the 43 | // `Output`, `HashType`, `SigHashes` and `InputIndex` fields are 44 | // populated within the sign descriptors. 45 | ComputeInputScript(ctx context.Context, tx *wire.MsgTx, 46 | signDescriptors []*SignDescriptor, prevOutputs []*wire.TxOut) ( 47 | []*input.Script, error) 48 | 49 | // SignMessage signs a message with the key specified in the key 50 | // locator. The returned signature is fixed-size LN wire format encoded. 51 | SignMessage(ctx context.Context, msg []byte, 52 | locator keychain.KeyLocator, opts ...SignMessageOption) ([]byte, 53 | error) 54 | 55 | // VerifyMessage verifies a signature over a message using the public 56 | // key provided. The signature must be fixed-size LN wire format 57 | // encoded. 58 | VerifyMessage(ctx context.Context, msg, sig []byte, pubkey [33]byte, 59 | opts ...VerifyMessageOption) (bool, error) 60 | 61 | // DeriveSharedKey returns a shared secret key by performing 62 | // Diffie-Hellman key derivation between the ephemeral public key and 63 | // the key specified by the key locator (or the node's identity private 64 | // key if no key locator is specified): 65 | // 66 | // P_shared = privKeyNode * ephemeralPubkey 67 | // 68 | // The resulting shared public key is serialized in the compressed 69 | // format and hashed with SHA256, resulting in a final key length of 256 70 | // bits. 71 | DeriveSharedKey(ctx context.Context, ephemeralPubKey *btcec.PublicKey, 72 | keyLocator *keychain.KeyLocator) ([32]byte, error) 73 | 74 | // MuSig2CreateSession creates a new musig session with the key and 75 | // signers provided. Note that depending on the version the signer keys 76 | // may need to be either 33 byte public keys or 32 byte Schnorr public 77 | // keys. 78 | MuSig2CreateSession(ctx context.Context, version input.MuSig2Version, 79 | signerLoc *keychain.KeyLocator, signers [][]byte, 80 | opts ...MuSig2SessionOpts) (*input.MuSig2SessionInfo, error) 81 | 82 | // MuSig2RegisterNonces registers additional public nonces for a musig2 83 | // session. It returns a boolean indicating whether we have all of our 84 | // nonces present. 85 | MuSig2RegisterNonces(ctx context.Context, sessionID [32]byte, 86 | nonces [][66]byte) (bool, error) 87 | 88 | // MuSig2Sign creates a partial signature for the 32 byte SHA256 digest 89 | // of a message. This can only be called once all public nonces have 90 | // been created. If the caller will not be responsible for combining 91 | // the signatures, the cleanup bool should be set. 92 | MuSig2Sign(ctx context.Context, sessionID [32]byte, 93 | message [32]byte, cleanup bool) ([]byte, error) 94 | 95 | // MuSig2CombineSig combines the given partial signature(s) with the 96 | // local one, if it already exists. Once a partial signature of all 97 | // participants are registered, the final signature will be combined 98 | // and returned. 99 | MuSig2CombineSig(ctx context.Context, sessionID [32]byte, 100 | otherPartialSigs [][]byte) (bool, []byte, error) 101 | 102 | // MuSig2Cleanup removes a session from memory to free up resources. 103 | MuSig2Cleanup(ctx context.Context, sessionID [32]byte) error 104 | } 105 | 106 | // SignDescriptor houses the necessary information required to successfully 107 | // sign a given segwit output. This struct is used by the Signer interface in 108 | // order to gain access to critical data needed to generate a valid signature. 109 | type SignDescriptor struct { 110 | // KeyDesc is a descriptor that precisely describes *which* key to use 111 | // for signing. This may provide the raw public key directly, or 112 | // require the Signer to re-derive the key according to the populated 113 | // derivation path. 114 | KeyDesc keychain.KeyDescriptor 115 | 116 | // SingleTweak is a scalar value that will be added to the private key 117 | // corresponding to the above public key to obtain the private key to 118 | // be used to sign this input. This value is typically derived via the 119 | // following computation: 120 | // 121 | // * derivedKey = privkey + sha256(perCommitmentPoint || pubKey) mod N 122 | // 123 | // NOTE: If this value is nil, then the input can be signed using only 124 | // the above public key. Either a SingleTweak should be set or a 125 | // DoubleTweak, not both. 126 | SingleTweak []byte 127 | 128 | // DoubleTweak is a private key that will be used in combination with 129 | // its corresponding private key to derive the private key that is to 130 | // be used to sign the target input. Within the Lightning protocol, 131 | // this value is typically the commitment secret from a previously 132 | // revoked commitment transaction. This value is in combination with 133 | // two hash values, and the original private key to derive the private 134 | // key to be used when signing. 135 | // 136 | // * k = (privKey*sha256(pubKey || tweakPub) + 137 | // tweakPriv*sha256(tweakPub || pubKey)) mod N 138 | // 139 | // NOTE: If this value is nil, then the input can be signed using only 140 | // the above public key. Either a SingleTweak should be set or a 141 | // DoubleTweak, not both. 142 | DoubleTweak *btcec.PrivateKey 143 | 144 | // The 32 byte input to the taproot tweak derivation that is used to 145 | // derive the output key from an internal key: outputKey = internalKey + 146 | // tagged_hash("tapTweak", internalKey || tapTweak). 147 | // 148 | // When doing a BIP 86 spend, this field can be an empty byte slice. 149 | // 150 | // When doing a normal key path spend, with the output key committing to 151 | // an actual script root, then this field should be: the tapscript root 152 | // hash. 153 | TapTweak []byte 154 | 155 | // WitnessScript is the full script required to properly redeem the 156 | // output. This field should be set to the full script if a p2wsh or 157 | // p2tr output is being signed. For p2wkh it should be set to the hashed 158 | // script (PkScript), for p2tr this should be the raw leaf script that's 159 | // being spent. 160 | WitnessScript []byte 161 | 162 | // TaprootKeySpend specifies how the input should be signed. Depending 163 | // on the method, either the tap_tweak, witness_script or both need to 164 | // be specified. Defaults to SegWit v0 signing to be backward compatible 165 | // with older RPC clients. 166 | SignMethod input.SignMethod 167 | 168 | // Output is the target output which should be signed. The PkScript and 169 | // Value fields within the output should be properly populated, 170 | // otherwise an invalid signature may be generated. 171 | Output *wire.TxOut 172 | 173 | // HashType is the target sighash type that should be used when 174 | // generating the final sighash, and signature. 175 | HashType txscript.SigHashType 176 | 177 | // InputIndex is the target input within the transaction that should be 178 | // signed. 179 | InputIndex int 180 | } 181 | 182 | // MarshalSignMethod turns the native sign method into the RPC counterpart. 183 | func MarshalSignMethod(signMethod input.SignMethod) signrpc.SignMethod { 184 | switch signMethod { 185 | case input.TaprootKeySpendBIP0086SignMethod: 186 | return signrpc.SignMethod_SIGN_METHOD_TAPROOT_KEY_SPEND_BIP0086 187 | 188 | case input.TaprootKeySpendSignMethod: 189 | return signrpc.SignMethod_SIGN_METHOD_TAPROOT_KEY_SPEND 190 | 191 | case input.TaprootScriptSpendSignMethod: 192 | return signrpc.SignMethod_SIGN_METHOD_TAPROOT_SCRIPT_SPEND 193 | 194 | default: 195 | return signrpc.SignMethod_SIGN_METHOD_WITNESS_V0 196 | } 197 | } 198 | 199 | type signerClient struct { 200 | client signrpc.SignerClient 201 | signerMac serializedMacaroon 202 | timeout time.Duration 203 | } 204 | 205 | // A compile time check to ensure that signerClient implements the SignerClient 206 | // interface. 207 | var _ SignerClient = (*signerClient)(nil) 208 | 209 | func newSignerClient(conn grpc.ClientConnInterface, 210 | signerMac serializedMacaroon, timeout time.Duration) *signerClient { 211 | 212 | return &signerClient{ 213 | client: signrpc.NewSignerClient(conn), 214 | signerMac: signerMac, 215 | timeout: timeout, 216 | } 217 | } 218 | 219 | // RawClientWithMacAuth returns a context with the proper macaroon 220 | // authentication, the default RPC timeout, and the raw client. 221 | func (s *signerClient) RawClientWithMacAuth( 222 | parentCtx context.Context) (context.Context, time.Duration, 223 | signrpc.SignerClient) { 224 | 225 | return s.signerMac.WithMacaroonAuth(parentCtx), s.timeout, s.client 226 | } 227 | 228 | func marshallSignDescriptors(signDescriptors []*SignDescriptor, 229 | fullDescriptors bool) []*signrpc.SignDescriptor { 230 | 231 | // partialDescriptor is a helper method that creates a partially 232 | // populated sign descriptor that is backward compatible with the way 233 | // some applications like Loop expect the call to lnd to be made. This 234 | // function only populates _either_ the public key or the key locator in 235 | // the descriptor, but not both. 236 | partialDescriptor := func( 237 | d keychain.KeyDescriptor) *signrpc.KeyDescriptor { 238 | 239 | keyDesc := &signrpc.KeyDescriptor{} 240 | if d.PubKey != nil { 241 | keyDesc.RawKeyBytes = d.PubKey.SerializeCompressed() 242 | } else { 243 | keyDesc.KeyLoc = &signrpc.KeyLocator{ 244 | KeyFamily: int32(d.KeyLocator.Family), 245 | KeyIndex: int32(d.KeyLocator.Index), 246 | } 247 | } 248 | 249 | return keyDesc 250 | } 251 | 252 | // fullDescriptor is a helper method that creates a fully populated sign 253 | // descriptor that includes both the public key and the key locator (if 254 | // available). For the locator we explicitly check that both the family 255 | // _and_ the index is non-zero. In some applications it's possible that 256 | // the family is always set (because only a specific family is used), 257 | // but the index might be zero because it's the first key, or because it 258 | // isn't known at that particular moment. 259 | // We aim to be compatible with this method in lnd's wallet: 260 | // https://github.com/lightningnetwork/lnd/blob/master/lnwallet/btcwallet/signer.go#L286 261 | // Because we know all custom families (0 to 255) are derived at wallet 262 | // creation, and the very first index of each family/account is always 263 | // derived, we know that only using the public key for that very first 264 | // index will work. But for a freshly initialized wallet (e.g. restored 265 | // from seed), we won't know any indexes greater than 0, so we _need_ to 266 | // also specify the key locator and not just the public key. 267 | fullDescriptor := func( 268 | d keychain.KeyDescriptor) *signrpc.KeyDescriptor { 269 | 270 | keyDesc := &signrpc.KeyDescriptor{} 271 | if d.PubKey != nil { 272 | keyDesc.RawKeyBytes = d.PubKey.SerializeCompressed() 273 | } 274 | 275 | if d.KeyLocator.Family != 0 && d.KeyLocator.Index != 0 { 276 | keyDesc.KeyLoc = &signrpc.KeyLocator{ 277 | KeyFamily: int32(d.KeyLocator.Family), 278 | KeyIndex: int32(d.KeyLocator.Index), 279 | } 280 | } 281 | 282 | return keyDesc 283 | } 284 | 285 | rpcSignDescs := make([]*signrpc.SignDescriptor, len(signDescriptors)) 286 | for i, signDesc := range signDescriptors { 287 | keyDesc := partialDescriptor(signDesc.KeyDesc) 288 | if fullDescriptors { 289 | keyDesc = fullDescriptor(signDesc.KeyDesc) 290 | } 291 | 292 | var doubleTweak []byte 293 | if signDesc.DoubleTweak != nil { 294 | doubleTweak = signDesc.DoubleTweak.Serialize() 295 | } 296 | 297 | rpcSignDescs[i] = &signrpc.SignDescriptor{ 298 | WitnessScript: signDesc.WitnessScript, 299 | SignMethod: MarshalSignMethod(signDesc.SignMethod), 300 | Output: &signrpc.TxOut{ 301 | PkScript: signDesc.Output.PkScript, 302 | Value: signDesc.Output.Value, 303 | }, 304 | Sighash: uint32(signDesc.HashType), 305 | InputIndex: int32(signDesc.InputIndex), 306 | KeyDesc: keyDesc, 307 | SingleTweak: signDesc.SingleTweak, 308 | DoubleTweak: doubleTweak, 309 | TapTweak: signDesc.TapTweak, 310 | } 311 | } 312 | 313 | return rpcSignDescs 314 | } 315 | 316 | // marshallTxOut marshals the transaction outputs as their RPC counterparts. 317 | func marshallTxOut(outputs []*wire.TxOut) []*signrpc.TxOut { 318 | rpcOutputs := make([]*signrpc.TxOut, len(outputs)) 319 | for i, output := range outputs { 320 | rpcOutputs[i] = &signrpc.TxOut{ 321 | PkScript: output.PkScript, 322 | Value: output.Value, 323 | } 324 | } 325 | 326 | return rpcOutputs 327 | } 328 | 329 | // SignOutputRaw is a method that can be used to generate a signature for a set 330 | // of inputs/outputs to a transaction. Each request specifies details concerning 331 | // how the outputs should be signed, which keys they should be signed with, and 332 | // also any optional tweaks. 333 | func (s *signerClient) SignOutputRaw(ctx context.Context, tx *wire.MsgTx, 334 | signDescriptors []*SignDescriptor, prevOutputs []*wire.TxOut) ([][]byte, 335 | error) { 336 | 337 | return s.signOutputRaw(ctx, tx, signDescriptors, prevOutputs, false) 338 | } 339 | 340 | // SignOutputRawKeyLocator is a copy of the SignOutputRaw that fixes a specific 341 | // issue around how the key locator is populated in the sign descriptor. We copy 342 | // this method instead of fixing the original to make sure we don't break any 343 | // existing applications that have already adjusted themselves to use the 344 | // specific behavior of the original SignOutputRaw method. 345 | func (s *signerClient) SignOutputRawKeyLocator(ctx context.Context, 346 | tx *wire.MsgTx, signDescriptors []*SignDescriptor, 347 | prevOutputs []*wire.TxOut) ([][]byte, error) { 348 | 349 | return s.signOutputRaw(ctx, tx, signDescriptors, prevOutputs, true) 350 | } 351 | 352 | // signOutputRaw is a helper method that performs the actual signing of the 353 | // transaction. 354 | func (s *signerClient) signOutputRaw(ctx context.Context, tx *wire.MsgTx, 355 | signDescriptors []*SignDescriptor, prevOutputs []*wire.TxOut, 356 | fullDescriptor bool) ([][]byte, error) { 357 | 358 | txRaw, err := encodeTx(tx) 359 | if err != nil { 360 | return nil, err 361 | } 362 | rpcSignDescs := marshallSignDescriptors(signDescriptors, fullDescriptor) 363 | rpcPrevOutputs := marshallTxOut(prevOutputs) 364 | 365 | rpcCtx, cancel := context.WithTimeout(ctx, s.timeout) 366 | defer cancel() 367 | 368 | rpcCtx = s.signerMac.WithMacaroonAuth(rpcCtx) 369 | resp, err := s.client.SignOutputRaw(rpcCtx, 370 | &signrpc.SignReq{ 371 | RawTxBytes: txRaw, 372 | SignDescs: rpcSignDescs, 373 | PrevOutputs: rpcPrevOutputs, 374 | }, 375 | ) 376 | if err != nil { 377 | return nil, err 378 | } 379 | 380 | return resp.RawSigs, nil 381 | } 382 | 383 | // ComputeInputScript generates the proper input script for P2TR, P2WPKH and 384 | // NP2WPKH outputs. This method only requires that the `Output`, `HashType`, 385 | // `SigHashes` and `InputIndex` fields are populated within the sign 386 | // descriptors. Passing in the previous outputs is required when spending one 387 | // or more taproot (SegWit v1) outputs. 388 | func (s *signerClient) ComputeInputScript(ctx context.Context, tx *wire.MsgTx, 389 | signDescriptors []*SignDescriptor, prevOutputs []*wire.TxOut) ( 390 | []*input.Script, error) { 391 | 392 | txRaw, err := encodeTx(tx) 393 | if err != nil { 394 | return nil, err 395 | } 396 | rpcSignDescs := marshallSignDescriptors(signDescriptors, false) 397 | rpcPrevOutputs := marshallTxOut(prevOutputs) 398 | 399 | rpcCtx, cancel := context.WithTimeout(ctx, s.timeout) 400 | defer cancel() 401 | 402 | rpcCtx = s.signerMac.WithMacaroonAuth(rpcCtx) 403 | resp, err := s.client.ComputeInputScript( 404 | rpcCtx, &signrpc.SignReq{ 405 | RawTxBytes: txRaw, 406 | SignDescs: rpcSignDescs, 407 | PrevOutputs: rpcPrevOutputs, 408 | }, 409 | ) 410 | if err != nil { 411 | return nil, err 412 | } 413 | 414 | inputScripts := make([]*input.Script, 0, len(resp.InputScripts)) 415 | for _, inputScript := range resp.InputScripts { 416 | inputScripts = append(inputScripts, &input.Script{ 417 | SigScript: inputScript.SigScript, 418 | Witness: inputScript.Witness, 419 | }) 420 | } 421 | 422 | return inputScripts, nil 423 | } 424 | 425 | // SignMessageOption is a function type that allows the customization of a 426 | // SignMessage RPC request. 427 | type SignMessageOption func(req *signrpc.SignMessageReq) 428 | 429 | // SignCompact sets the flag for returning a compact signature in the message 430 | // request. 431 | func SignCompact() SignMessageOption { 432 | return func(req *signrpc.SignMessageReq) { 433 | req.CompactSig = true 434 | } 435 | } 436 | 437 | // SignSchnorr sets the flag for returning a Schnorr signature in the message 438 | // request. 439 | func SignSchnorr(taprootTweak []byte) SignMessageOption { 440 | return func(req *signrpc.SignMessageReq) { 441 | req.SchnorrSig = true 442 | req.SchnorrSigTapTweak = taprootTweak 443 | } 444 | } 445 | 446 | // SignMessage signs a message with the key specified in the key locator. The 447 | // returned signature is fixed-size LN wire format encoded. 448 | func (s *signerClient) SignMessage(ctx context.Context, msg []byte, 449 | locator keychain.KeyLocator, opts ...SignMessageOption) ([]byte, error) { 450 | 451 | rpcCtx, cancel := context.WithTimeout(ctx, s.timeout) 452 | defer cancel() 453 | 454 | rpcIn := &signrpc.SignMessageReq{ 455 | Msg: msg, 456 | KeyLoc: &signrpc.KeyLocator{ 457 | KeyFamily: int32(locator.Family), 458 | KeyIndex: int32(locator.Index), 459 | }, 460 | } 461 | 462 | for _, opt := range opts { 463 | opt(rpcIn) 464 | } 465 | 466 | rpcCtx = s.signerMac.WithMacaroonAuth(rpcCtx) 467 | resp, err := s.client.SignMessage(rpcCtx, rpcIn) 468 | if err != nil { 469 | return nil, err 470 | } 471 | 472 | return resp.Signature, nil 473 | } 474 | 475 | // VerifyMessageOption is a function type that allows the customization of a 476 | // VerifyMessage RPC request. 477 | type VerifyMessageOption func(req *signrpc.VerifyMessageReq) 478 | 479 | // VerifySchnorr sets the flag for checking a Schnorr signature in the message 480 | // request. 481 | func VerifySchnorr() VerifyMessageOption { 482 | return func(req *signrpc.VerifyMessageReq) { 483 | req.IsSchnorrSig = true 484 | } 485 | } 486 | 487 | // VerifyMessage verifies a signature over a message using the public key 488 | // provided. The signature must be fixed-size LN wire format encoded. 489 | func (s *signerClient) VerifyMessage(ctx context.Context, msg, sig []byte, 490 | pubkey [33]byte, opts ...VerifyMessageOption) (bool, error) { 491 | 492 | rpcCtx, cancel := context.WithTimeout(ctx, s.timeout) 493 | defer cancel() 494 | 495 | rpcIn := &signrpc.VerifyMessageReq{ 496 | Msg: msg, 497 | Signature: sig, 498 | Pubkey: pubkey[:], 499 | } 500 | 501 | for _, opt := range opts { 502 | opt(rpcIn) 503 | } 504 | 505 | // If the signature is a Schnorr signature, we need to set the public 506 | // key as the 32-byte x-only key, as mentioned in the RPC docs. 507 | if rpcIn.IsSchnorrSig { 508 | rpcIn.Pubkey = pubkey[1:] 509 | } 510 | 511 | rpcCtx = s.signerMac.WithMacaroonAuth(rpcCtx) 512 | resp, err := s.client.VerifyMessage(rpcCtx, rpcIn) 513 | if err != nil { 514 | return false, err 515 | } 516 | return resp.Valid, nil 517 | } 518 | 519 | // DeriveSharedKey returns a shared secret key by performing Diffie-Hellman key 520 | // derivation between the ephemeral public key and the key specified by the key 521 | // locator (or the node's identity private key if no key locator is specified): 522 | // 523 | // P_shared = privKeyNode * ephemeralPubkey 524 | // 525 | // The resulting shared public key is serialized in the compressed format and 526 | // hashed with SHA256, resulting in a final key length of 256 bits. 527 | func (s *signerClient) DeriveSharedKey(ctx context.Context, 528 | ephemeralPubKey *btcec.PublicKey, 529 | keyLocator *keychain.KeyLocator) ([32]byte, error) { 530 | 531 | rpcCtx, cancel := context.WithTimeout(ctx, s.timeout) 532 | defer cancel() 533 | 534 | rpcIn := &signrpc.SharedKeyRequest{ 535 | EphemeralPubkey: ephemeralPubKey.SerializeCompressed(), 536 | KeyLoc: &signrpc.KeyLocator{ 537 | KeyFamily: int32(keyLocator.Family), 538 | KeyIndex: int32(keyLocator.Index), 539 | }, 540 | } 541 | 542 | rpcCtx = s.signerMac.WithMacaroonAuth(rpcCtx) 543 | resp, err := s.client.DeriveSharedKey(rpcCtx, rpcIn) 544 | if err != nil { 545 | return [32]byte{}, err 546 | } 547 | 548 | var sharedKey [32]byte 549 | copy(sharedKey[:], resp.SharedKey) 550 | return sharedKey, nil 551 | } 552 | 553 | // MuSig2SessionOpts is the signature used to apply functional options to 554 | // musig session requests. 555 | type MuSig2SessionOpts func(*signrpc.MuSig2SessionRequest) 556 | 557 | // noncesToBytes converts a set of public nonces to a [][]byte. 558 | func noncesToBytes(nonces [][musig2.PubNonceSize]byte) [][]byte { 559 | nonceBytes := make([][]byte, len(nonces)) 560 | 561 | for i := range nonces { 562 | nonceBytes[i] = nonces[i][:] 563 | } 564 | 565 | return nonceBytes 566 | } 567 | 568 | // MuSig2NonceOpt adds an optional set of nonces to a musig session request. 569 | func MuSig2NonceOpt(nonces [][musig2.PubNonceSize]byte) MuSig2SessionOpts { 570 | return func(s *signrpc.MuSig2SessionRequest) { 571 | s.OtherSignerPublicNonces = noncesToBytes(nonces) 572 | } 573 | } 574 | 575 | // MuSig2TaprootTweakOpt adds an optional taproot tweak to the musig session 576 | // request. 577 | func MuSig2TaprootTweakOpt(scriptRoot []byte, 578 | keySpendOnly bool) MuSig2SessionOpts { 579 | 580 | return func(s *signrpc.MuSig2SessionRequest) { 581 | s.TaprootTweak = &signrpc.TaprootTweakDesc{ 582 | ScriptRoot: scriptRoot, 583 | KeySpendOnly: keySpendOnly, 584 | } 585 | } 586 | } 587 | 588 | // MuSig2LocalNonceOpt adds the local secret nonce to the musig session request. 589 | func MuSig2LocalNonceOpt(nonce [musig2.SecNonceSize]byte) MuSig2SessionOpts { 590 | return func(s *signrpc.MuSig2SessionRequest) { 591 | s.PregeneratedLocalNonce = nonce[:] 592 | } 593 | } 594 | 595 | // marshallMuSig2Version translates the passed input.MuSig2Version value to 596 | // signrpc.MuSig2Version. 597 | func marshallMuSig2Version(version input.MuSig2Version) ( 598 | signrpc.MuSig2Version, error) { 599 | 600 | // Select the version based on the passed Go enum. Note that with new 601 | // versions added this switch must be updated as RPC enum values are 602 | // not directly mapped to the Go enum values defined in the input 603 | // package. 604 | switch version { 605 | case input.MuSig2Version040: 606 | return signrpc.MuSig2Version_MUSIG2_VERSION_V040, nil 607 | 608 | case input.MuSig2Version100RC2: 609 | return signrpc.MuSig2Version_MUSIG2_VERSION_V100RC2, nil 610 | 611 | default: 612 | return signrpc.MuSig2Version_MUSIG2_VERSION_UNDEFINED, 613 | fmt.Errorf("invalid MuSig2 version") 614 | } 615 | } 616 | 617 | // MuSig2CreateSession creates a new musig session with the key and signers 618 | // provided. 619 | func (s *signerClient) MuSig2CreateSession(ctx context.Context, 620 | version input.MuSig2Version, signerLoc *keychain.KeyLocator, 621 | signers [][]byte, opts ...MuSig2SessionOpts) ( 622 | *input.MuSig2SessionInfo, error) { 623 | 624 | rpcMuSig2Version, err := marshallMuSig2Version(version) 625 | if err != nil { 626 | return nil, err 627 | } 628 | 629 | req := &signrpc.MuSig2SessionRequest{ 630 | KeyLoc: &signrpc.KeyLocator{ 631 | KeyFamily: int32(signerLoc.Family), 632 | KeyIndex: int32(signerLoc.Index), 633 | }, 634 | AllSignerPubkeys: signers, 635 | Version: rpcMuSig2Version, 636 | } 637 | 638 | for _, opt := range opts { 639 | opt(req) 640 | } 641 | 642 | rpcCtx, cancel := context.WithTimeout(ctx, s.timeout) 643 | defer cancel() 644 | 645 | rpcCtx = s.signerMac.WithMacaroonAuth(rpcCtx) 646 | resp, err := s.client.MuSig2CreateSession(rpcCtx, req) 647 | if err != nil { 648 | return nil, err 649 | } 650 | 651 | combinedKey, err := schnorr.ParsePubKey(resp.CombinedKey) 652 | if err != nil { 653 | return nil, fmt.Errorf("could not parse combined key: %v", err) 654 | } 655 | 656 | session := &input.MuSig2SessionInfo{ 657 | CombinedKey: combinedKey, 658 | HaveAllNonces: resp.HaveAllNonces, 659 | } 660 | 661 | if len(resp.LocalPublicNonces) != musig2.PubNonceSize { 662 | return nil, fmt.Errorf("unexpected local nonce size: %v", 663 | len(resp.LocalPublicNonces)) 664 | } 665 | copy(session.PublicNonce[:], resp.LocalPublicNonces) 666 | 667 | if len(resp.SessionId) != 32 { 668 | return nil, fmt.Errorf("unexpected session ID length: %v", 669 | len(resp.SessionId)) 670 | } 671 | 672 | copy(session.SessionID[:], resp.SessionId) 673 | 674 | return session, nil 675 | } 676 | 677 | // MuSig2RegisterNonces registers additional public nonces for a musig2 session. 678 | // It returns a boolean indicating whether we have all of our nonces present. 679 | func (s *signerClient) MuSig2RegisterNonces(ctx context.Context, 680 | sessionID [32]byte, nonces [][66]byte) (bool, error) { 681 | 682 | req := &signrpc.MuSig2RegisterNoncesRequest{ 683 | SessionId: sessionID[:], 684 | OtherSignerPublicNonces: noncesToBytes(nonces), 685 | } 686 | 687 | rpcCtx, cancel := context.WithTimeout(ctx, s.timeout) 688 | defer cancel() 689 | 690 | rpcCtx = s.signerMac.WithMacaroonAuth(rpcCtx) 691 | resp, err := s.client.MuSig2RegisterNonces(rpcCtx, req) 692 | if err != nil { 693 | return false, err 694 | } 695 | 696 | return resp.HaveAllNonces, nil 697 | } 698 | 699 | // MuSig2Sign creates a partial signature for the 32 byte SHA256 digest of a 700 | // message. This can only be called once all public nonces have been created. 701 | // If the caller will not be responsible for combining the signatures, the 702 | // cleanup bool should be set. 703 | func (s *signerClient) MuSig2Sign(ctx context.Context, sessionID [32]byte, 704 | message [32]byte, cleanup bool) ([]byte, error) { 705 | 706 | req := &signrpc.MuSig2SignRequest{ 707 | SessionId: sessionID[:], 708 | MessageDigest: message[:], 709 | Cleanup: cleanup, 710 | } 711 | 712 | rpcCtx, cancel := context.WithTimeout(ctx, s.timeout) 713 | defer cancel() 714 | 715 | rpcCtx = s.signerMac.WithMacaroonAuth(rpcCtx) 716 | resp, err := s.client.MuSig2Sign(rpcCtx, req) 717 | if err != nil { 718 | return nil, err 719 | } 720 | 721 | return resp.LocalPartialSignature, nil 722 | } 723 | 724 | // MuSig2CombineSig combines the given partial signature(s) with the local one, 725 | // if it already exists. Once a partial signature of all participants are 726 | // registered, the final signature will be combined and returned. 727 | func (s *signerClient) MuSig2CombineSig(ctx context.Context, sessionID [32]byte, 728 | otherPartialSigs [][]byte) (bool, []byte, error) { 729 | 730 | req := &signrpc.MuSig2CombineSigRequest{ 731 | SessionId: sessionID[:], 732 | OtherPartialSignatures: otherPartialSigs, 733 | } 734 | 735 | rpcCtx, cancel := context.WithTimeout(ctx, s.timeout) 736 | defer cancel() 737 | 738 | rpcCtx = s.signerMac.WithMacaroonAuth(rpcCtx) 739 | resp, err := s.client.MuSig2CombineSig(rpcCtx, req) 740 | if err != nil { 741 | return false, nil, err 742 | } 743 | 744 | return resp.HaveAllSignatures, resp.FinalSignature, nil 745 | } 746 | 747 | // MuSig2Cleanup allows a caller to clean up a session early in case where it's 748 | // obvious that the signing session won't succeed and the resources can be 749 | // released. 750 | func (s *signerClient) MuSig2Cleanup(ctx context.Context, 751 | sessionID [32]byte) error { 752 | 753 | req := &signrpc.MuSig2CleanupRequest{ 754 | SessionId: sessionID[:], 755 | } 756 | rpcCtx, cancel := context.WithTimeout(ctx, s.timeout) 757 | defer cancel() 758 | 759 | rpcCtx = s.signerMac.WithMacaroonAuth(rpcCtx) 760 | _, err := s.client.MuSig2Cleanup(rpcCtx, req) 761 | 762 | return err 763 | } 764 | -------------------------------------------------------------------------------- /state_client.go: -------------------------------------------------------------------------------- 1 | package lndclient 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | "time" 8 | 9 | "github.com/lightningnetwork/lnd/lnrpc" 10 | "google.golang.org/grpc" 11 | ) 12 | 13 | // StateClient exposes base lightning functionality. 14 | type StateClient interface { 15 | ServiceClient[lnrpc.StateClient] 16 | 17 | // SubscribeState subscribes to the current state of the wallet. 18 | SubscribeState(ctx context.Context) (chan WalletState, chan error, 19 | error) 20 | 21 | // GetState returns the current wallet state without subscribing to more 22 | // state updates. 23 | GetState(context.Context) (WalletState, error) 24 | } 25 | 26 | // WalletState is a type that represents all states the lnd wallet can be in. 27 | type WalletState uint8 28 | 29 | const ( 30 | // WalletStateNonExisting denotes that no wallet has been created in lnd 31 | // so far. 32 | WalletStateNonExisting WalletState = 0 33 | 34 | // WalletStateLocked denotes that a wallet exists in lnd but it has not 35 | // yet been unlocked. 36 | WalletStateLocked WalletState = 1 37 | 38 | // WalletStateUnlocked denotes that a wallet exists in lnd and it has 39 | // been unlocked but the RPC server isn't yet fully started up. 40 | WalletStateUnlocked WalletState = 2 41 | 42 | // WalletStateRPCActive denotes that lnd is now fully ready to receive 43 | // RPC requests other than wallet unlocking operations. 44 | WalletStateRPCActive WalletState = 3 45 | 46 | // WalletStateServerActive denotes that lnd's main server is now fully 47 | // ready to receive calls. 48 | WalletStateServerActive WalletState = 4 49 | 50 | // WalletStateWaitingToStart indicates that lnd is at the beginning of 51 | // the startup process. In a cluster environment this may mean that 52 | // we're waiting to become the leader in which case RPC calls will be 53 | // disabled until this instance has been elected as leader. 54 | WalletStateWaitingToStart WalletState = 255 55 | ) 56 | 57 | // String returns a string representation of the WalletState. 58 | func (s WalletState) String() string { 59 | switch s { 60 | case WalletStateNonExisting: 61 | return "No wallet exists" 62 | 63 | case WalletStateLocked: 64 | return "Wallet is locked" 65 | 66 | case WalletStateUnlocked: 67 | return "Wallet is unlocked" 68 | 69 | case WalletStateRPCActive: 70 | return "Lnd RPC server is ready for requests" 71 | 72 | case WalletStateServerActive: 73 | return "Lnd main server is ready for requests" 74 | 75 | case WalletStateWaitingToStart: 76 | return "Lnd is waiting to start" 77 | 78 | default: 79 | return fmt.Sprintf("unknown wallet state <%d>", s) 80 | } 81 | } 82 | 83 | // ReadyForGetInfo returns true if the wallet state is ready for the GetInfo to 84 | // be called. This needs to also return true for the RPC active state to be 85 | // backward compatible with lnd 0.13.x nodes which didn't yet have the server 86 | // active state. But the GetInfo RPC isn't guarded by that server active flag 87 | // anyway, so we can call that whenever the RPC server is ready. 88 | func (s WalletState) ReadyForGetInfo() bool { 89 | return s == WalletStateRPCActive || s == WalletStateServerActive 90 | } 91 | 92 | // stateClient is a client for lnd's lnrpc.State service. 93 | type stateClient struct { 94 | client lnrpc.StateClient 95 | readonlyMac serializedMacaroon 96 | timeout time.Duration 97 | 98 | wg sync.WaitGroup 99 | } 100 | 101 | // A compile time check to ensure that stateClient implements the StateClient 102 | // interface. 103 | var _ StateClient = (*stateClient)(nil) 104 | 105 | // newStateClient returns a new stateClient. 106 | func newStateClient(conn grpc.ClientConnInterface, 107 | readonlyMac serializedMacaroon, timeout time.Duration) *stateClient { 108 | 109 | return &stateClient{ 110 | client: lnrpc.NewStateClient(conn), 111 | readonlyMac: readonlyMac, 112 | timeout: timeout, 113 | } 114 | } 115 | 116 | // WaitForFinished waits until all state subscriptions have finished. 117 | func (s *stateClient) WaitForFinished() { 118 | s.wg.Wait() 119 | } 120 | 121 | // RawClientWithMacAuth returns a context with the proper macaroon 122 | // authentication, the default RPC timeout, and the raw client. 123 | func (s *stateClient) RawClientWithMacAuth( 124 | parentCtx context.Context) (context.Context, time.Duration, 125 | lnrpc.StateClient) { 126 | 127 | return s.readonlyMac.WithMacaroonAuth(parentCtx), s.timeout, s.client 128 | } 129 | 130 | // SubscribeState subscribes to the current state of the wallet. 131 | func (s *stateClient) SubscribeState(ctx context.Context) (chan WalletState, 132 | chan error, error) { 133 | 134 | resp, err := s.client.SubscribeState( 135 | ctx, &lnrpc.SubscribeStateRequest{}, 136 | ) 137 | if err != nil { 138 | return nil, nil, err 139 | } 140 | 141 | stateChan := make(chan WalletState, 1) 142 | errChan := make(chan error, 1) 143 | 144 | s.wg.Add(1) 145 | go func() { 146 | defer s.wg.Done() 147 | 148 | for { 149 | stateEvent, err := resp.Recv() 150 | if err != nil { 151 | errChan <- err 152 | return 153 | } 154 | 155 | state, err := unmarshalWalletState(stateEvent.State) 156 | if err != nil { 157 | errChan <- err 158 | return 159 | } 160 | 161 | select { 162 | case stateChan <- state: 163 | case <-ctx.Done(): 164 | return 165 | } 166 | 167 | // If this is the final state, no more states will be 168 | // sent to us, and we can close the subscription. 169 | if state == WalletStateServerActive { 170 | close(stateChan) 171 | close(errChan) 172 | 173 | return 174 | } 175 | } 176 | }() 177 | 178 | return stateChan, errChan, nil 179 | } 180 | 181 | // GetState returns the current wallet state without subscribing to more 182 | // state updates. 183 | func (s *stateClient) GetState(ctx context.Context) (WalletState, error) { 184 | state, err := s.client.GetState(ctx, &lnrpc.GetStateRequest{}) 185 | if err != nil { 186 | return 0, err 187 | } 188 | 189 | return unmarshalWalletState(state.State) 190 | } 191 | 192 | // unmarshalWalletState turns the RPC wallet state into the internal wallet 193 | // state type. 194 | func unmarshalWalletState(rpcState lnrpc.WalletState) (WalletState, error) { 195 | switch rpcState { 196 | case lnrpc.WalletState_WAITING_TO_START: 197 | return WalletStateWaitingToStart, nil 198 | 199 | case lnrpc.WalletState_NON_EXISTING: 200 | return WalletStateNonExisting, nil 201 | 202 | case lnrpc.WalletState_LOCKED: 203 | return WalletStateLocked, nil 204 | 205 | case lnrpc.WalletState_UNLOCKED: 206 | return WalletStateUnlocked, nil 207 | 208 | case lnrpc.WalletState_RPC_ACTIVE: 209 | return WalletStateRPCActive, nil 210 | 211 | case lnrpc.WalletState_SERVER_ACTIVE: 212 | return WalletStateServerActive, nil 213 | 214 | default: 215 | return 0, fmt.Errorf("unknown wallet state: %d", rpcState) 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /tools/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.23.6-bookworm 2 | 3 | RUN apt-get update && apt-get install -y git 4 | ENV GOCACHE=/tmp/build/.cache 5 | ENV GOMODCACHE=/tmp/build/.modcache 6 | ENV GOFLAGS="-buildvcs=false" 7 | 8 | COPY . /tmp/tools 9 | 10 | RUN cd /tmp \ 11 | && mkdir -p /tmp/build/.cache \ 12 | && mkdir -p /tmp/build/.modcache \ 13 | && cd /tmp/tools \ 14 | && go install -trimpath github.com/golangci/golangci-lint/cmd/golangci-lint \ 15 | && chmod -R 777 /tmp/build/ 16 | 17 | WORKDIR /build 18 | -------------------------------------------------------------------------------- /tools/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lightninglabs/lndclient/tools 2 | 3 | go 1.23.6 4 | 5 | require ( 6 | github.com/golangci/golangci-lint v1.64.7 7 | github.com/rinchsan/gosimports v0.1.5 8 | ) 9 | 10 | require ( 11 | 4d63.com/gocheckcompilerdirectives v1.3.0 // indirect 12 | 4d63.com/gochecknoglobals v0.2.2 // indirect 13 | github.com/4meepo/tagalign v1.4.2 // indirect 14 | github.com/Abirdcfly/dupword v0.1.3 // indirect 15 | github.com/Antonboom/errname v1.0.0 // indirect 16 | github.com/Antonboom/nilnil v1.0.1 // indirect 17 | github.com/Antonboom/testifylint v1.5.2 // indirect 18 | github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect 19 | github.com/Crocmagnon/fatcontext v0.7.1 // indirect 20 | github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect 21 | github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1 // indirect 22 | github.com/Masterminds/semver/v3 v3.3.0 // indirect 23 | github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect 24 | github.com/alecthomas/go-check-sumtype v0.3.1 // indirect 25 | github.com/alexkohler/nakedret/v2 v2.0.5 // indirect 26 | github.com/alexkohler/prealloc v1.0.0 // indirect 27 | github.com/alingse/asasalint v0.0.11 // indirect 28 | github.com/alingse/nilnesserr v0.1.2 // indirect 29 | github.com/ashanbrown/forbidigo v1.6.0 // indirect 30 | github.com/ashanbrown/makezero v1.2.0 // indirect 31 | github.com/beorn7/perks v1.0.1 // indirect 32 | github.com/bkielbasa/cyclop v1.2.3 // indirect 33 | github.com/blizzy78/varnamelen v0.8.0 // indirect 34 | github.com/bombsimon/wsl/v4 v4.5.0 // indirect 35 | github.com/breml/bidichk v0.3.2 // indirect 36 | github.com/breml/errchkjson v0.4.0 // indirect 37 | github.com/butuzov/ireturn v0.3.1 // indirect 38 | github.com/butuzov/mirror v1.3.0 // indirect 39 | github.com/catenacyber/perfsprint v0.8.2 // indirect 40 | github.com/ccojocar/zxcvbn-go v1.0.2 // indirect 41 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 42 | github.com/charithe/durationcheck v0.0.10 // indirect 43 | github.com/chavacava/garif v0.1.0 // indirect 44 | github.com/ckaznocha/intrange v0.3.0 // indirect 45 | github.com/curioswitch/go-reassign v0.3.0 // indirect 46 | github.com/daixiang0/gci v0.13.5 // indirect 47 | github.com/davecgh/go-spew v1.1.1 // indirect 48 | github.com/denis-tingaikin/go-header v0.5.0 // indirect 49 | github.com/ettle/strcase v0.2.0 // indirect 50 | github.com/fatih/color v1.18.0 // indirect 51 | github.com/fatih/structtag v1.2.0 // indirect 52 | github.com/firefart/nonamedreturns v1.0.5 // indirect 53 | github.com/fsnotify/fsnotify v1.5.4 // indirect 54 | github.com/fzipp/gocyclo v0.6.0 // indirect 55 | github.com/ghostiam/protogetter v0.3.9 // indirect 56 | github.com/go-critic/go-critic v0.12.0 // indirect 57 | github.com/go-toolsmith/astcast v1.1.0 // indirect 58 | github.com/go-toolsmith/astcopy v1.1.0 // indirect 59 | github.com/go-toolsmith/astequal v1.2.0 // indirect 60 | github.com/go-toolsmith/astfmt v1.1.0 // indirect 61 | github.com/go-toolsmith/astp v1.1.0 // indirect 62 | github.com/go-toolsmith/strparse v1.1.0 // indirect 63 | github.com/go-toolsmith/typep v1.1.0 // indirect 64 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect 65 | github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect 66 | github.com/gobwas/glob v0.2.3 // indirect 67 | github.com/gofrs/flock v0.12.1 // indirect 68 | github.com/golang/protobuf v1.5.3 // indirect 69 | github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect 70 | github.com/golangci/go-printf-func-name v0.1.0 // indirect 71 | github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d // indirect 72 | github.com/golangci/misspell v0.6.0 // indirect 73 | github.com/golangci/plugin-module-register v0.1.1 // indirect 74 | github.com/golangci/revgrep v0.8.0 // indirect 75 | github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed // indirect 76 | github.com/google/go-cmp v0.7.0 // indirect 77 | github.com/gordonklaus/ineffassign v0.1.0 // indirect 78 | github.com/gostaticanalysis/analysisutil v0.7.1 // indirect 79 | github.com/gostaticanalysis/comment v1.5.0 // indirect 80 | github.com/gostaticanalysis/forcetypeassert v0.2.0 // indirect 81 | github.com/gostaticanalysis/nilerr v0.1.1 // indirect 82 | github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect 83 | github.com/hashicorp/go-version v1.7.0 // indirect 84 | github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect 85 | github.com/hashicorp/hcl v1.0.0 // indirect 86 | github.com/hexops/gotextdiff v1.0.3 // indirect 87 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 88 | github.com/jgautheron/goconst v1.7.1 // indirect 89 | github.com/jingyugao/rowserrcheck v1.1.1 // indirect 90 | github.com/jjti/go-spancheck v0.6.4 // indirect 91 | github.com/julz/importas v0.2.0 // indirect 92 | github.com/karamaru-alpha/copyloopvar v1.2.1 // indirect 93 | github.com/kisielk/errcheck v1.9.0 // indirect 94 | github.com/kkHAIKE/contextcheck v1.1.6 // indirect 95 | github.com/kulti/thelper v0.6.3 // indirect 96 | github.com/kunwardeep/paralleltest v1.0.10 // indirect 97 | github.com/lasiar/canonicalheader v1.1.2 // indirect 98 | github.com/ldez/exptostd v0.4.2 // indirect 99 | github.com/ldez/gomoddirectives v0.6.1 // indirect 100 | github.com/ldez/grignotin v0.9.0 // indirect 101 | github.com/ldez/tagliatelle v0.7.1 // indirect 102 | github.com/ldez/usetesting v0.4.2 // indirect 103 | github.com/leonklingele/grouper v1.1.2 // indirect 104 | github.com/macabu/inamedparam v0.1.3 // indirect 105 | github.com/magiconair/properties v1.8.6 // indirect 106 | github.com/maratori/testableexamples v1.0.0 // indirect 107 | github.com/maratori/testpackage v1.1.1 // indirect 108 | github.com/matoous/godox v1.1.0 // indirect 109 | github.com/mattn/go-colorable v0.1.14 // indirect 110 | github.com/mattn/go-isatty v0.0.20 // indirect 111 | github.com/mattn/go-runewidth v0.0.16 // indirect 112 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect 113 | github.com/mgechev/revive v1.7.0 // indirect 114 | github.com/mitchellh/go-homedir v1.1.0 // indirect 115 | github.com/mitchellh/mapstructure v1.5.0 // indirect 116 | github.com/moricho/tparallel v0.3.2 // indirect 117 | github.com/nakabonne/nestif v0.3.1 // indirect 118 | github.com/nishanths/exhaustive v0.12.0 // indirect 119 | github.com/nishanths/predeclared v0.2.2 // indirect 120 | github.com/nunnatsa/ginkgolinter v0.19.1 // indirect 121 | github.com/olekukonko/tablewriter v0.0.5 // indirect 122 | github.com/pelletier/go-toml v1.9.5 // indirect 123 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 124 | github.com/pmezard/go-difflib v1.0.0 // indirect 125 | github.com/polyfloyd/go-errorlint v1.7.1 // indirect 126 | github.com/prometheus/client_golang v1.12.1 // indirect 127 | github.com/prometheus/client_model v0.2.0 // indirect 128 | github.com/prometheus/common v0.32.1 // indirect 129 | github.com/prometheus/procfs v0.7.3 // indirect 130 | github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 // indirect 131 | github.com/quasilyte/go-ruleguard/dsl v0.3.22 // indirect 132 | github.com/quasilyte/gogrep v0.5.0 // indirect 133 | github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect 134 | github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect 135 | github.com/raeperd/recvcheck v0.2.0 // indirect 136 | github.com/rivo/uniseg v0.4.7 // indirect 137 | github.com/rogpeppe/go-internal v1.14.1 // indirect 138 | github.com/ryancurrah/gomodguard v1.3.5 // indirect 139 | github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect 140 | github.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect 141 | github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect 142 | github.com/sashamelentyev/interfacebloat v1.1.0 // indirect 143 | github.com/sashamelentyev/usestdlibvars v1.28.0 // indirect 144 | github.com/securego/gosec/v2 v2.22.2 // indirect 145 | github.com/sirupsen/logrus v1.9.3 // indirect 146 | github.com/sivchari/containedctx v1.0.3 // indirect 147 | github.com/sivchari/tenv v1.12.1 // indirect 148 | github.com/sonatard/noctx v0.1.0 // indirect 149 | github.com/sourcegraph/go-diff v0.7.0 // indirect 150 | github.com/spf13/afero v1.12.0 // indirect 151 | github.com/spf13/cast v1.5.0 // indirect 152 | github.com/spf13/cobra v1.9.1 // indirect 153 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 154 | github.com/spf13/pflag v1.0.6 // indirect 155 | github.com/spf13/viper v1.12.0 // indirect 156 | github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect 157 | github.com/stbenjam/no-sprintf-host-port v0.2.0 // indirect 158 | github.com/stretchr/objx v0.5.2 // indirect 159 | github.com/stretchr/testify v1.10.0 // indirect 160 | github.com/subosito/gotenv v1.4.1 // indirect 161 | github.com/tdakkota/asciicheck v0.4.1 // indirect 162 | github.com/tetafro/godot v1.5.0 // indirect 163 | github.com/timakin/bodyclose v0.0.0-20241017074812-ed6a65f985e3 // indirect 164 | github.com/timonwong/loggercheck v0.10.1 // indirect 165 | github.com/tomarrell/wrapcheck/v2 v2.10.0 // indirect 166 | github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect 167 | github.com/ultraware/funlen v0.2.0 // indirect 168 | github.com/ultraware/whitespace v0.2.0 // indirect 169 | github.com/uudashr/gocognit v1.2.0 // indirect 170 | github.com/uudashr/iface v1.3.1 // indirect 171 | github.com/xen0n/gosmopolitan v1.2.2 // indirect 172 | github.com/yagipy/maintidx v1.0.0 // indirect 173 | github.com/yeya24/promlinter v0.3.0 // indirect 174 | github.com/ykadowak/zerologlint v0.1.5 // indirect 175 | gitlab.com/bosi/decorder v0.4.2 // indirect 176 | go-simpler.org/musttag v0.13.0 // indirect 177 | go-simpler.org/sloglint v0.9.0 // indirect 178 | go.uber.org/atomic v1.7.0 // indirect 179 | go.uber.org/automaxprocs v1.6.0 // indirect 180 | go.uber.org/multierr v1.6.0 // indirect 181 | go.uber.org/zap v1.24.0 // indirect 182 | golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac // indirect 183 | golang.org/x/mod v0.24.0 // indirect 184 | golang.org/x/sync v0.12.0 // indirect 185 | golang.org/x/sys v0.31.0 // indirect 186 | golang.org/x/text v0.22.0 // indirect 187 | golang.org/x/tools v0.31.0 // indirect 188 | google.golang.org/protobuf v1.36.5 // indirect 189 | gopkg.in/ini.v1 v1.67.0 // indirect 190 | gopkg.in/yaml.v2 v2.4.0 // indirect 191 | gopkg.in/yaml.v3 v3.0.1 // indirect 192 | honnef.co/go/tools v0.6.1 // indirect 193 | mvdan.cc/gofumpt v0.7.0 // indirect 194 | mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f // indirect 195 | ) 196 | -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | // +build tools 3 | 4 | package lndclient 5 | 6 | // The other imports represent our build tools. Instead of defining a commit we 7 | // want to use for those golang based tools, we use the go mod versioning system 8 | // to unify the way we manage dependencies. So we define our build tool 9 | // dependencies here and pin the version in go.mod. 10 | import ( 11 | _ "github.com/golangci/golangci-lint/cmd/golangci-lint" 12 | _ "github.com/rinchsan/gosimports/cmd/gosimports" 13 | ) 14 | -------------------------------------------------------------------------------- /tx_utils.go: -------------------------------------------------------------------------------- 1 | package lndclient 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/btcsuite/btcd/chaincfg/chainhash" 11 | "github.com/btcsuite/btcd/wire" 12 | ) 13 | 14 | // encodeTx encodes a tx to raw bytes. 15 | func encodeTx(tx *wire.MsgTx) ([]byte, error) { 16 | var buffer bytes.Buffer 17 | err := tx.BtcEncode(&buffer, 0, wire.WitnessEncoding) 18 | if err != nil { 19 | return nil, err 20 | } 21 | rawTx := buffer.Bytes() 22 | 23 | return rawTx, nil 24 | } 25 | 26 | // decodeTx decodes raw tx bytes. 27 | func decodeTx(rawTx []byte) (*wire.MsgTx, error) { 28 | tx := wire.MsgTx{} 29 | r := bytes.NewReader(rawTx) 30 | err := tx.BtcDecode(r, 0, wire.WitnessEncoding) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | return &tx, nil 36 | } 37 | 38 | // decodeBlock decodes a raw block into a struct. 39 | func decodeBlock(rawBlock []byte) (*wire.MsgBlock, error) { 40 | var block wire.MsgBlock 41 | err := block.Deserialize(bytes.NewReader(rawBlock)) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | return &block, nil 47 | } 48 | 49 | // NewOutpointFromStr creates an outpoint from a string with the format 50 | // txid:index. 51 | func NewOutpointFromStr(outpoint string) (*wire.OutPoint, error) { 52 | parts := strings.Split(outpoint, ":") 53 | if len(parts) != 2 { 54 | return nil, errors.New("outpoint should be of the form txid:index") 55 | } 56 | hash, err := chainhash.NewHashFromStr(parts[0]) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | outputIndex, err := strconv.Atoi(parts[1]) 62 | if err != nil { 63 | return nil, fmt.Errorf("invalid output index: %v", err) 64 | } 65 | 66 | return &wire.OutPoint{ 67 | Hash: *hash, 68 | Index: uint32(outputIndex), 69 | }, nil 70 | } 71 | -------------------------------------------------------------------------------- /versioner_client.go: -------------------------------------------------------------------------------- 1 | package lndclient 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | "time" 8 | 9 | "github.com/lightningnetwork/lnd/lnrpc/verrpc" 10 | "google.golang.org/grpc" 11 | ) 12 | 13 | // VersionerClient exposes the version of lnd. 14 | type VersionerClient interface { 15 | ServiceClient[verrpc.VersionerClient] 16 | 17 | // GetVersion returns the version and build information of the lnd 18 | // daemon. 19 | GetVersion(ctx context.Context) (*verrpc.Version, error) 20 | } 21 | 22 | type versionerClient struct { 23 | client verrpc.VersionerClient 24 | readonlyMac serializedMacaroon 25 | timeout time.Duration 26 | } 27 | 28 | // A compile time check to ensure that versionerClient implements the 29 | // VersionerClient interface. 30 | var _ VersionerClient = (*versionerClient)(nil) 31 | 32 | func newVersionerClient(conn grpc.ClientConnInterface, 33 | readonlyMac serializedMacaroon, timeout time.Duration) *versionerClient { 34 | 35 | return &versionerClient{ 36 | client: verrpc.NewVersionerClient(conn), 37 | readonlyMac: readonlyMac, 38 | timeout: timeout, 39 | } 40 | } 41 | 42 | // RawClientWithMacAuth returns a context with the proper macaroon 43 | // authentication, the default RPC timeout, and the raw client. 44 | func (v *versionerClient) RawClientWithMacAuth( 45 | parentCtx context.Context) (context.Context, time.Duration, 46 | verrpc.VersionerClient) { 47 | 48 | return v.readonlyMac.WithMacaroonAuth(parentCtx), v.timeout, v.client 49 | } 50 | 51 | // GetVersion returns the version and build information of the lnd 52 | // daemon. 53 | // 54 | // NOTE: This method is part of the VersionerClient interface. 55 | func (v *versionerClient) GetVersion(ctx context.Context) (*verrpc.Version, 56 | error) { 57 | 58 | rpcCtx, cancel := context.WithTimeout( 59 | v.readonlyMac.WithMacaroonAuth(ctx), v.timeout, 60 | ) 61 | defer cancel() 62 | return v.client.GetVersion(rpcCtx, &verrpc.VersionRequest{}) 63 | } 64 | 65 | // VersionString returns a nice, human-readable string of a version returned by 66 | // the VersionerClient, including all build tags. 67 | func VersionString(version *verrpc.Version) string { 68 | short := VersionStringShort(version) 69 | enabledTags := strings.Join(version.BuildTags, ",") 70 | return fmt.Sprintf("%s, build tags '%s'", short, enabledTags) 71 | } 72 | 73 | // VersionStringShort returns a nice, human-readable string of a version 74 | // returned by the VersionerClient. 75 | func VersionStringShort(version *verrpc.Version) string { 76 | versionStr := fmt.Sprintf( 77 | "v%d.%d.%d", version.AppMajor, version.AppMinor, 78 | version.AppPatch, 79 | ) 80 | if version.AppPreRelease != "" { 81 | versionStr = fmt.Sprintf( 82 | "%s-%s", versionStr, version.AppPreRelease, 83 | ) 84 | } 85 | return versionStr 86 | } 87 | --------------------------------------------------------------------------------