|
| 1 | +# Remote signing |
| 2 | + |
| 3 | +Remote signing refers to an operating mode of `lnd` in which the wallet is |
| 4 | +segregated into two parts, each running within its own instance of `lnd`. One |
| 5 | +instance is running in watch-only mode which means it only has **public** |
| 6 | +keys in its wallet. The second instance (in this document referred to as |
| 7 | +"signer" or "remote signer" instance) has the same keys in its wallet, including |
| 8 | +the **private** keys. |
| 9 | + |
| 10 | +The advantage of such a setup is that the `lnd` instance containing the private |
| 11 | +keys (the "signer") can be completely offline except for a single inbound gRPC |
| 12 | +connection and the outbound connection to `bitcoind` (or `btcd` or `neutrino`). |
| 13 | +The signer instance can run on a different machine with more tightly locked down |
| 14 | +network security, optimally only allowing the single gRPC connection from the |
| 15 | +outside. |
| 16 | + |
| 17 | +An example setup could look like: |
| 18 | + |
| 19 | +```text |
| 20 | + xxxxxxxxx |
| 21 | + xxxxxxxxx xxxx |
| 22 | +xxx xx |
| 23 | +x LN p2p network xx |
| 24 | +x x |
| 25 | +xxx xx |
| 26 | + xxxxx xxxxxxxx |
| 27 | + xxx |
| 28 | + ^ +----------------------------------+ |
| 29 | + | p2p traffic | firewalled/offline network zone | |
| 30 | + | | | |
| 31 | + v | | |
| 32 | + +----------------+ gRPC | +----------------+ | |
| 33 | + | watch-only lnd +--------------+-->| full seed lnd | | |
| 34 | + +-------+--------+ | +------+---------+ | |
| 35 | + | | | | |
| 36 | + +-------v--------+ | +------v---------+ | |
| 37 | + | bitcoind/btcd | | | bitcoind/btcd | | |
| 38 | + +----------------+ | +----------------+ | |
| 39 | + | | |
| 40 | + +----------------------------------+ |
| 41 | +``` |
| 42 | + |
| 43 | +NOTE: Offline in this context means that `lnd` itself does not need to be |
| 44 | +reachable from the internet and itself doesn't connect out. If the `bitcoind` |
| 45 | +or other chain backend is indeed running within this same firewall/offline zone |
| 46 | +then that component will need at least _outbound_ internet access. But it is |
| 47 | +also possible to use a chain backend that is running outside this zone. |
| 48 | + |
| 49 | +## Restrictions / limitations |
| 50 | + |
| 51 | +The current implementation of the remote signing mode comes with a few |
| 52 | +restrictions and limitations: |
| 53 | + |
| 54 | +- Both `lnd` instances need a connection to a chain backend: |
| 55 | + - The type of chain backend (`bitcoind`, `btcd` or `neutrino`) **must** |
| 56 | + match. |
| 57 | + - Both instances can point to the same chain backend node, though limits |
| 58 | + apply with the number of `lnd` instances that can use the same `bitcoind` |
| 59 | + node over ZMQ. |
| 60 | + See ["running multiple lnd nodes" in the safety guide](safety.md#running-multiple-lnd-nodes) |
| 61 | + . |
| 62 | + - Using a pruned chain backend is not recommended as that increases the |
| 63 | + chance of the two wallets falling out of sync with each other. |
| 64 | +- The wallet of the "signer" instance **must not be used** for anything. |
| 65 | + Especially generating new addresses manually on the "signer" will lead to the |
| 66 | + two wallets falling out of sync! |
| 67 | + |
| 68 | +## Example setup |
| 69 | + |
| 70 | +In this example we are going to set up two nodes, the "signer" that has the full |
| 71 | +seed and private keys and the "watch-only" node that only has public keys. |
| 72 | + |
| 73 | +### The "signer" node |
| 74 | + |
| 75 | +The node "signer" is the hardened node that contains the private key material |
| 76 | +and is not connected to the internet or LN P2P network at all. Ideally only a |
| 77 | +single RPC based connection (that can be firewalled off specifically) can be |
| 78 | +opened to this node from the host on which the node "watch-only" is running. |
| 79 | + |
| 80 | +Recommended entries in `lnd.conf`: |
| 81 | + |
| 82 | +```text |
| 83 | +# No special configuration required other than basic "hardening" parameters to |
| 84 | +# make sure no connections to the internet are opened. |
| 85 | +
|
| 86 | +[Application Options] |
| 87 | +# Don't listen on the p2p port. |
| 88 | +nolisten=true |
| 89 | +
|
| 90 | +# Don't reach out to the bootstrap nodes, we don't need a synced graph. |
| 91 | +nobootstrap=true |
| 92 | +
|
| 93 | +# Just an example, this is the port that needs to be opened in the firewall and |
| 94 | +# reachable from the node "watch-only". |
| 95 | +rpclisten=10019 |
| 96 | +``` |
| 97 | + |
| 98 | +After successfully starting up "signer", the following command can be run to |
| 99 | +export the `xpub`s of the wallet: |
| 100 | + |
| 101 | +```shell |
| 102 | +signer> ⛰ lncli wallet accounts list > accounts-signer.json |
| 103 | +``` |
| 104 | + |
| 105 | +That `accounts-signer.json` file has to be copied to the machine on which |
| 106 | +"watch-only" will be running. It contains the extended public keys for all of |
| 107 | +`lnd`'s accounts. |
| 108 | + |
| 109 | +A custom macaroon can be baked for the watch-only node so it only gets the |
| 110 | +minimum required permissions on the signer instance: |
| 111 | + |
| 112 | +```shell |
| 113 | +signer> ⛰ lncli bakemacaroon --save_to signer.custom.macaroon \ |
| 114 | + message:write signer:generate address:read onchain:write |
| 115 | +``` |
| 116 | + |
| 117 | +Copy this file (`signer.custom.macaroon`) along with the `tls.cert` of the |
| 118 | +signer node to the machine where the watch-only node will be running. |
| 119 | + |
| 120 | +### The "watch-only" node |
| 121 | + |
| 122 | +The node "watch-only" is the public, internet facing node that does not contain |
| 123 | +any private keys in its wallet but delegates all signing operations to the node |
| 124 | +"signer" over a single RPC connection. |
| 125 | + |
| 126 | +Required entries in `lnd.conf`: |
| 127 | + |
| 128 | +```text |
| 129 | +[remotesigner] |
| 130 | +remotesigner.enable=true |
| 131 | +remotesigner.rpchost=zane.example.internal:10019 |
| 132 | +remotesigner.tlscertpath=/home/watch-only/example/signer.tls.cert |
| 133 | +remotesigner.macaroonpath=/home/watch-only/example/signer.custom.macaroon |
| 134 | +``` |
| 135 | + |
| 136 | +After starting "watch-only", the wallet can be created in watch-only mode by |
| 137 | +running: |
| 138 | + |
| 139 | +```shell |
| 140 | +watch-only> ⛰ lncli createwatchonly accounts-signer.json |
| 141 | + |
| 142 | +Input wallet password: |
| 143 | +Confirm password: |
| 144 | + |
| 145 | +Input an optional wallet birthday unix timestamp of first block to start scanning from (default 0): |
| 146 | + |
| 147 | + |
| 148 | +Input an optional address look-ahead used to scan for used keys (default 2500): |
| 149 | +``` |
| 150 | + |
| 151 | +Alternatively a script can be used for initializing the watch-only wallet |
| 152 | +through the RPC interface as is described in the next section. |
| 153 | + |
| 154 | +## Example initialization script |
| 155 | + |
| 156 | +This section shows an example script that initializes the watch-only wallet of |
| 157 | +the public node using NodeJS. |
| 158 | + |
| 159 | +To use this example, first initialize the "signer" wallet with the root key |
| 160 | +`tprv8ZgxMBicQKsPe6jS4vDm2n7s42Q6MpvghUQqMmSKG7bTZvGKtjrcU3PGzMNG37yzxywrcdvgkwrr8eYXJmbwdvUNVT4Ucv7ris4jvA7BUmg` |
| 161 | +using the command line. This can be done by using the new `x` option during the |
| 162 | +interactive `lncli create` command: |
| 163 | + |
| 164 | +```bash |
| 165 | +signer> ⛰ lncli create |
| 166 | +Input wallet password: |
| 167 | +Confirm password: |
| 168 | + |
| 169 | +Do you have an existing cipher seed mnemonic or extended master root key you want to use? |
| 170 | +Enter 'y' to use an existing cipher seed mnemonic, 'x' to use an extended master root key |
| 171 | +or 'n' to create a new seed (Enter y/x/n): |
| 172 | +``` |
| 173 | + |
| 174 | +Then run this script against the "watch-only" node (after editing the |
| 175 | +constants): |
| 176 | + |
| 177 | +```javascript |
| 178 | + |
| 179 | +// EDIT ME: |
| 180 | +const WATCH_ONLY_LND_DIR = '/home/watch-only/.lnd'; |
| 181 | +const WATCH_ONLY_RPC_HOSTPORT = 'localhost:10018'; |
| 182 | +const WATCH_ONLY_WALLET_PASSWORD = 'testnet3'; |
| 183 | +const LND_SOURCE_DIR = '.'; |
| 184 | + |
| 185 | +const fs = require('fs'); |
| 186 | +const grpc = require('@grpc/grpc-js'); |
| 187 | +const protoLoader = require('@grpc/proto-loader'); |
| 188 | +const loaderOptions = { |
| 189 | + keepCase: true, |
| 190 | + longs: String, |
| 191 | + enums: String, |
| 192 | + defaults: true, |
| 193 | + oneofs: true |
| 194 | +}; |
| 195 | +const packageDefinition = protoLoader.loadSync([ |
| 196 | + LND_SOURCE_DIR + '/lnrpc/walletunlocker.proto', |
| 197 | +], loaderOptions); |
| 198 | + |
| 199 | +process.env.GRPC_SSL_CIPHER_SUITES = 'HIGH+ECDSA' |
| 200 | + |
| 201 | +// build ssl credentials using the cert the same as before |
| 202 | +let lndCert = fs.readFileSync(WATCH_ONLY_LND_DIR + '/tls.cert'); |
| 203 | +let sslCreds = grpc.credentials.createSsl(lndCert); |
| 204 | + |
| 205 | +let lnrpcDescriptor = grpc.loadPackageDefinition(packageDefinition); |
| 206 | +let lnrpc = lnrpcDescriptor.lnrpc; |
| 207 | +var client = new lnrpc.WalletUnlocker(WATCH_ONLY_RPC_HOSTPORT, sslCreds); |
| 208 | + |
| 209 | +client.initWallet({ |
| 210 | + wallet_password: Buffer.from(WATCH_ONLY_WALLET_PASSWORD, 'utf-8'), |
| 211 | + recovery_window: 2500, |
| 212 | + watch_only: { |
| 213 | + accounts: [{ |
| 214 | + purpose: 49, |
| 215 | + coin_type: 0, |
| 216 | + account: 0, |
| 217 | + xpub: 'tpubDDXEYWvGCTytEF6hBog9p4qr2QBUvJhh4P2wM4qHHv9N489khkQoGkBXDVoquuiyBf8SKBwrYseYdtq9j2v2nttPpE8qbuW3sE2MCkFPhTq', |
| 218 | + }, { |
| 219 | + purpose: 84, |
| 220 | + coin_type: 0, |
| 221 | + account: 0, |
| 222 | + xpub: 'tpubDDWAWrSLRSFrG1KdqXMQQyTKYGSKLKaY7gxpvK7RdV3e3DkhvuW2GgsFvsPN4RGmuoYtUgZ1LHZE8oftz7T4mzc1BxGt5rt8zJcVQiKTPPV', |
| 223 | + }, { |
| 224 | + purpose: 1017, |
| 225 | + coin_type: 1, |
| 226 | + account: 0, |
| 227 | + xpub: 'tpubDDXFHr67Ro2tHKVWG2gNjjijKUH1Lyv5NKFYdJnuaLGVNBVwyV5AbykhR43iy8wYozEMbw2QfmAqZhb8gnuL5mm9sZh8YsR6FjGAbew1xoT', |
| 228 | + }, { |
| 229 | + purpose: 1017, |
| 230 | + coin_type: 1, |
| 231 | + account: 1, |
| 232 | + xpub: 'tpubDDXFHr67Ro2tKkccDqNfDqZpd5wCs2n6XRV2Uh185DzCTbkDaEd9v7P837zZTYBNVfaRriuxgGVgxbGjDui4CKxyzBzwz4aAZxjn2PhNcQy', |
| 233 | + }, { |
| 234 | + purpose: 1017, |
| 235 | + coin_type: 1, |
| 236 | + account: 2, |
| 237 | + xpub: 'tpubDDXFHr67Ro2tNH4KH41i4oTsWfRjFigoH1Ee7urvHow51opH9xJ7mu1qSPMPVtkVqQZ5tE4NTuFJPrbDqno7TQietyUDmPTwyVviJbGCwXk', |
| 238 | + }, { |
| 239 | + purpose: 1017, |
| 240 | + coin_type: 1, |
| 241 | + account: 3, |
| 242 | + xpub: 'tpubDDXFHr67Ro2tQj5Zvav2ALhkU6dRQAhEtNPnYJVBC8hs2U1A9ecqxRY3XTiJKBDD7e8tudhmTRs8aGWJAiAXJN5kXy3Hi6cmiwGWjXK5Cv5', |
| 243 | + }, { |
| 244 | + purpose: 1017, |
| 245 | + coin_type: 1, |
| 246 | + account: 4, |
| 247 | + xpub: 'tpubDDXFHr67Ro2tSSR2LLBJtotxx2U45cuESLWKA72YT9td3SzVKHAptzDEx5chsUNZ4WRMY5h6HJxRSebjRatxQKX1uUsux1LvKS1wsfNJ2PH', |
| 248 | + }, { |
| 249 | + purpose: 1017, |
| 250 | + coin_type: 1, |
| 251 | + account: 5, |
| 252 | + xpub: 'tpubDDXFHr67Ro2tTwzfWvNoMoPpZbxdMEfe1WhbXJxvXikGixPa4ggSRZeGx6T5yxVHTVT3rjVh35Veqsowj7emX8SZfXKDKDKcLduXCeWPUU3', |
| 253 | + }, { |
| 254 | + purpose: 1017, |
| 255 | + coin_type: 1, |
| 256 | + account: 6, |
| 257 | + xpub: 'tpubDDXFHr67Ro2tYEDS2EByRedfsUoEwBtrzVbS1qdPrX6sAkUYGLrZWvMmQv8KZDZ4zd9r8WzM9bJ2nGp7XuNVC4w2EBtWg7i76gbrmuEWjQh', |
| 258 | + }, { |
| 259 | + purpose: 1017, |
| 260 | + coin_type: 1, |
| 261 | + account: 7, |
| 262 | + xpub: 'tpubDDXFHr67Ro2tYpwnFJEQaM8eAPM2UV5uY6gFgXeSzS5aC5T9TfzXuawYKBbQMZJn8qHXLafY4tAutoda1aKP5h6Nbgy3swPbnhWbFjS5wnX', |
| 263 | + }, { |
| 264 | + purpose: 1017, |
| 265 | + coin_type: 1, |
| 266 | + account: 8, |
| 267 | + xpub: 'tpubDDXFHr67Ro2tddKpAjUegXqt7EGxRXnHkeLbUkfuFMGbLJYgRpG4ew5pMmGg2nmcGmHFQ29w3juNhd8N5ZZ8HwJdymC4f5ukQLJ4yg9rEr3', |
| 268 | + }, { |
| 269 | + purpose: 1017, |
| 270 | + coin_type: 1, |
| 271 | + account: 9, |
| 272 | + xpub: 'tpubDDXFHr67Ro2tgE89V8ZdgMytC2Jq1iT9ttGhdzR1X7haQJNBmXt8kau6taC6DGASYzbrjmo9z9w6JQFcaLNqbhS2h2PVSzKf79j265Zi8hF', |
| 273 | + }] |
| 274 | + } |
| 275 | +}, (err, res) => { |
| 276 | + if (err != null) { |
| 277 | + console.log(err); |
| 278 | + } |
| 279 | + console.log(res); |
| 280 | +}); |
| 281 | +``` |
0 commit comments