2021-10-14 15:43:02 +02:00
|
|
|
# Remote signing
|
|
|
|
|
|
|
|
Remote signing refers to an operating mode of `lnd` in which the wallet is
|
|
|
|
segregated into two parts, each running within its own instance of `lnd`. One
|
|
|
|
instance is running in watch-only mode which means it only has **public**
|
|
|
|
keys in its wallet. The second instance (in this document referred to as
|
|
|
|
"signer" or "remote signer" instance) has the same keys in its wallet, including
|
|
|
|
the **private** keys.
|
|
|
|
|
|
|
|
The advantage of such a setup is that the `lnd` instance containing the private
|
|
|
|
keys (the "signer") can be completely offline except for a single inbound gRPC
|
2022-01-05 11:04:35 +01:00
|
|
|
connection.
|
2021-10-14 15:43:02 +02:00
|
|
|
The signer instance can run on a different machine with more tightly locked down
|
|
|
|
network security, optimally only allowing the single gRPC connection from the
|
|
|
|
outside.
|
|
|
|
|
|
|
|
An example setup could look like:
|
|
|
|
|
|
|
|
```text
|
|
|
|
xxxxxxxxx
|
|
|
|
xxxxxxxxx xxxx
|
|
|
|
xxx xx
|
|
|
|
x LN p2p network xx
|
|
|
|
x x
|
|
|
|
xxx xx
|
|
|
|
xxxxx xxxxxxxx
|
|
|
|
xxx
|
|
|
|
^ +----------------------------------+
|
|
|
|
| p2p traffic | firewalled/offline network zone |
|
|
|
|
| | |
|
|
|
|
v | |
|
|
|
|
+----------------+ gRPC | +----------------+ |
|
|
|
|
| watch-only lnd +--------------+-->| full seed lnd | |
|
2022-01-05 11:04:35 +01:00
|
|
|
+-------+--------+ | +----------------+ |
|
|
|
|
| | |
|
|
|
|
+-------v--------+ +----------------------------------+
|
|
|
|
| bitcoind/btcd |
|
|
|
|
+----------------+
|
2021-10-14 15:43:02 +02:00
|
|
|
|
2022-01-05 11:04:35 +01:00
|
|
|
```
|
2021-10-14 15:43:02 +02:00
|
|
|
|
|
|
|
## Example setup
|
|
|
|
|
|
|
|
In this example we are going to set up two nodes, the "signer" that has the full
|
|
|
|
seed and private keys and the "watch-only" node that only has public keys.
|
|
|
|
|
|
|
|
### The "signer" node
|
|
|
|
|
|
|
|
The node "signer" is the hardened node that contains the private key material
|
|
|
|
and is not connected to the internet or LN P2P network at all. Ideally only a
|
|
|
|
single RPC based connection (that can be firewalled off specifically) can be
|
|
|
|
opened to this node from the host on which the node "watch-only" is running.
|
|
|
|
|
|
|
|
Recommended entries in `lnd.conf`:
|
|
|
|
|
|
|
|
```text
|
2022-01-05 11:04:35 +01:00
|
|
|
# We apply some basic "hardening" parameters to make sure no connections to the
|
|
|
|
# internet are opened.
|
2021-10-14 15:43:02 +02:00
|
|
|
|
|
|
|
[Application Options]
|
|
|
|
# Don't listen on the p2p port.
|
|
|
|
nolisten=true
|
|
|
|
|
|
|
|
# Don't reach out to the bootstrap nodes, we don't need a synced graph.
|
|
|
|
nobootstrap=true
|
|
|
|
|
|
|
|
# Just an example, this is the port that needs to be opened in the firewall and
|
|
|
|
# reachable from the node "watch-only".
|
|
|
|
rpclisten=10019
|
2022-01-05 11:04:35 +01:00
|
|
|
|
|
|
|
# The signer node will not look at the chain at all, it only needs to sign
|
|
|
|
# things with the keys contained in its wallet. So we don't need to hook it up
|
|
|
|
# to any chain backend.
|
|
|
|
[bitcoin]
|
|
|
|
# We still need to signal that we're using the Bitcoin chain.
|
|
|
|
bitcoin.active=true
|
|
|
|
|
|
|
|
# And we're making sure mainnet parameters are used.
|
|
|
|
bitcoin.mainnet=true
|
|
|
|
|
|
|
|
# But we aren't using a "real" chain backed but a mocked one.
|
|
|
|
bitcoin.node=nochainbackend
|
2021-10-14 15:43:02 +02:00
|
|
|
```
|
|
|
|
|
|
|
|
After successfully starting up "signer", the following command can be run to
|
|
|
|
export the `xpub`s of the wallet:
|
|
|
|
|
|
|
|
```shell
|
|
|
|
signer> ⛰ lncli wallet accounts list > accounts-signer.json
|
|
|
|
```
|
|
|
|
|
|
|
|
That `accounts-signer.json` file has to be copied to the machine on which
|
|
|
|
"watch-only" will be running. It contains the extended public keys for all of
|
|
|
|
`lnd`'s accounts.
|
|
|
|
|
|
|
|
A custom macaroon can be baked for the watch-only node so it only gets the
|
|
|
|
minimum required permissions on the signer instance:
|
|
|
|
|
|
|
|
```shell
|
|
|
|
signer> ⛰ lncli bakemacaroon --save_to signer.custom.macaroon \
|
|
|
|
message:write signer:generate address:read onchain:write
|
|
|
|
```
|
|
|
|
|
|
|
|
Copy this file (`signer.custom.macaroon`) along with the `tls.cert` of the
|
|
|
|
signer node to the machine where the watch-only node will be running.
|
|
|
|
|
|
|
|
### The "watch-only" node
|
|
|
|
|
|
|
|
The node "watch-only" is the public, internet facing node that does not contain
|
|
|
|
any private keys in its wallet but delegates all signing operations to the node
|
|
|
|
"signer" over a single RPC connection.
|
|
|
|
|
|
|
|
Required entries in `lnd.conf`:
|
|
|
|
|
|
|
|
```text
|
|
|
|
[remotesigner]
|
|
|
|
remotesigner.enable=true
|
|
|
|
remotesigner.rpchost=zane.example.internal:10019
|
|
|
|
remotesigner.tlscertpath=/home/watch-only/example/signer.tls.cert
|
|
|
|
remotesigner.macaroonpath=/home/watch-only/example/signer.custom.macaroon
|
|
|
|
```
|
|
|
|
|
|
|
|
After starting "watch-only", the wallet can be created in watch-only mode by
|
|
|
|
running:
|
|
|
|
|
|
|
|
```shell
|
|
|
|
watch-only> ⛰ lncli createwatchonly accounts-signer.json
|
|
|
|
|
|
|
|
Input wallet password:
|
|
|
|
Confirm password:
|
|
|
|
|
|
|
|
Input an optional wallet birthday unix timestamp of first block to start scanning from (default 0):
|
|
|
|
|
|
|
|
|
|
|
|
Input an optional address look-ahead used to scan for used keys (default 2500):
|
|
|
|
```
|
|
|
|
|
|
|
|
Alternatively a script can be used for initializing the watch-only wallet
|
|
|
|
through the RPC interface as is described in the next section.
|
|
|
|
|
2022-01-05 11:04:35 +01:00
|
|
|
## Migrating an existing setup to remote signing
|
|
|
|
|
|
|
|
It is possible to migrate a node that is currently a standalone, normal node
|
|
|
|
with all private keys in its wallet to a setup that uses remote signing (with
|
|
|
|
a watch-only and a remote signer node).
|
|
|
|
|
|
|
|
To migrate an existing node, follow these steps:
|
|
|
|
1. Create a new "signer" node using the same seed as the existing node,
|
|
|
|
following the steps [mentioned above](#the-signer-node).
|
|
|
|
2. In the configuration of the existing node, add the configuration entries as
|
|
|
|
[shown above](#the-watch-only-node). But instead of creating a new wallet
|
|
|
|
(since one already exists), instruct `lnd` to migrate the existing wallet to
|
|
|
|
a watch-only one (by purging all private key material from it) by adding the
|
|
|
|
`remotesigner.migrate-wallet-to-watch-only=true` configuration entry.
|
|
|
|
|
2021-10-14 15:43:02 +02:00
|
|
|
## Example initialization script
|
|
|
|
|
|
|
|
This section shows an example script that initializes the watch-only wallet of
|
|
|
|
the public node using NodeJS.
|
|
|
|
|
|
|
|
To use this example, first initialize the "signer" wallet with the root key
|
|
|
|
`tprv8ZgxMBicQKsPe6jS4vDm2n7s42Q6MpvghUQqMmSKG7bTZvGKtjrcU3PGzMNG37yzxywrcdvgkwrr8eYXJmbwdvUNVT4Ucv7ris4jvA7BUmg`
|
|
|
|
using the command line. This can be done by using the new `x` option during the
|
|
|
|
interactive `lncli create` command:
|
|
|
|
|
|
|
|
```bash
|
|
|
|
signer> ⛰ lncli create
|
|
|
|
Input wallet password:
|
|
|
|
Confirm password:
|
|
|
|
|
|
|
|
Do you have an existing cipher seed mnemonic or extended master root key you want to use?
|
|
|
|
Enter 'y' to use an existing cipher seed mnemonic, 'x' to use an extended master root key
|
|
|
|
or 'n' to create a new seed (Enter y/x/n):
|
|
|
|
```
|
|
|
|
|
|
|
|
Then run this script against the "watch-only" node (after editing the
|
|
|
|
constants):
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
|
|
|
// EDIT ME:
|
|
|
|
const WATCH_ONLY_LND_DIR = '/home/watch-only/.lnd';
|
|
|
|
const WATCH_ONLY_RPC_HOSTPORT = 'localhost:10018';
|
|
|
|
const WATCH_ONLY_WALLET_PASSWORD = 'testnet3';
|
|
|
|
const LND_SOURCE_DIR = '.';
|
|
|
|
|
|
|
|
const fs = require('fs');
|
|
|
|
const grpc = require('@grpc/grpc-js');
|
|
|
|
const protoLoader = require('@grpc/proto-loader');
|
|
|
|
const loaderOptions = {
|
|
|
|
keepCase: true,
|
|
|
|
longs: String,
|
|
|
|
enums: String,
|
|
|
|
defaults: true,
|
|
|
|
oneofs: true
|
|
|
|
};
|
|
|
|
const packageDefinition = protoLoader.loadSync([
|
|
|
|
LND_SOURCE_DIR + '/lnrpc/walletunlocker.proto',
|
|
|
|
], loaderOptions);
|
|
|
|
|
|
|
|
process.env.GRPC_SSL_CIPHER_SUITES = 'HIGH+ECDSA'
|
|
|
|
|
|
|
|
// build ssl credentials using the cert the same as before
|
|
|
|
let lndCert = fs.readFileSync(WATCH_ONLY_LND_DIR + '/tls.cert');
|
|
|
|
let sslCreds = grpc.credentials.createSsl(lndCert);
|
|
|
|
|
|
|
|
let lnrpcDescriptor = grpc.loadPackageDefinition(packageDefinition);
|
|
|
|
let lnrpc = lnrpcDescriptor.lnrpc;
|
|
|
|
var client = new lnrpc.WalletUnlocker(WATCH_ONLY_RPC_HOSTPORT, sslCreds);
|
|
|
|
|
|
|
|
client.initWallet({
|
|
|
|
wallet_password: Buffer.from(WATCH_ONLY_WALLET_PASSWORD, 'utf-8'),
|
|
|
|
recovery_window: 2500,
|
|
|
|
watch_only: {
|
|
|
|
accounts: [{
|
|
|
|
purpose: 49,
|
|
|
|
coin_type: 0,
|
|
|
|
account: 0,
|
|
|
|
xpub: 'tpubDDXEYWvGCTytEF6hBog9p4qr2QBUvJhh4P2wM4qHHv9N489khkQoGkBXDVoquuiyBf8SKBwrYseYdtq9j2v2nttPpE8qbuW3sE2MCkFPhTq',
|
|
|
|
}, {
|
|
|
|
purpose: 84,
|
|
|
|
coin_type: 0,
|
|
|
|
account: 0,
|
|
|
|
xpub: 'tpubDDWAWrSLRSFrG1KdqXMQQyTKYGSKLKaY7gxpvK7RdV3e3DkhvuW2GgsFvsPN4RGmuoYtUgZ1LHZE8oftz7T4mzc1BxGt5rt8zJcVQiKTPPV',
|
|
|
|
}, {
|
|
|
|
purpose: 1017,
|
|
|
|
coin_type: 1,
|
|
|
|
account: 0,
|
|
|
|
xpub: 'tpubDDXFHr67Ro2tHKVWG2gNjjijKUH1Lyv5NKFYdJnuaLGVNBVwyV5AbykhR43iy8wYozEMbw2QfmAqZhb8gnuL5mm9sZh8YsR6FjGAbew1xoT',
|
|
|
|
}, {
|
|
|
|
purpose: 1017,
|
|
|
|
coin_type: 1,
|
|
|
|
account: 1,
|
|
|
|
xpub: 'tpubDDXFHr67Ro2tKkccDqNfDqZpd5wCs2n6XRV2Uh185DzCTbkDaEd9v7P837zZTYBNVfaRriuxgGVgxbGjDui4CKxyzBzwz4aAZxjn2PhNcQy',
|
|
|
|
}, {
|
|
|
|
purpose: 1017,
|
|
|
|
coin_type: 1,
|
|
|
|
account: 2,
|
|
|
|
xpub: 'tpubDDXFHr67Ro2tNH4KH41i4oTsWfRjFigoH1Ee7urvHow51opH9xJ7mu1qSPMPVtkVqQZ5tE4NTuFJPrbDqno7TQietyUDmPTwyVviJbGCwXk',
|
|
|
|
}, {
|
|
|
|
purpose: 1017,
|
|
|
|
coin_type: 1,
|
|
|
|
account: 3,
|
|
|
|
xpub: 'tpubDDXFHr67Ro2tQj5Zvav2ALhkU6dRQAhEtNPnYJVBC8hs2U1A9ecqxRY3XTiJKBDD7e8tudhmTRs8aGWJAiAXJN5kXy3Hi6cmiwGWjXK5Cv5',
|
|
|
|
}, {
|
|
|
|
purpose: 1017,
|
|
|
|
coin_type: 1,
|
|
|
|
account: 4,
|
|
|
|
xpub: 'tpubDDXFHr67Ro2tSSR2LLBJtotxx2U45cuESLWKA72YT9td3SzVKHAptzDEx5chsUNZ4WRMY5h6HJxRSebjRatxQKX1uUsux1LvKS1wsfNJ2PH',
|
|
|
|
}, {
|
|
|
|
purpose: 1017,
|
|
|
|
coin_type: 1,
|
|
|
|
account: 5,
|
|
|
|
xpub: 'tpubDDXFHr67Ro2tTwzfWvNoMoPpZbxdMEfe1WhbXJxvXikGixPa4ggSRZeGx6T5yxVHTVT3rjVh35Veqsowj7emX8SZfXKDKDKcLduXCeWPUU3',
|
|
|
|
}, {
|
|
|
|
purpose: 1017,
|
|
|
|
coin_type: 1,
|
|
|
|
account: 6,
|
|
|
|
xpub: 'tpubDDXFHr67Ro2tYEDS2EByRedfsUoEwBtrzVbS1qdPrX6sAkUYGLrZWvMmQv8KZDZ4zd9r8WzM9bJ2nGp7XuNVC4w2EBtWg7i76gbrmuEWjQh',
|
|
|
|
}, {
|
|
|
|
purpose: 1017,
|
|
|
|
coin_type: 1,
|
|
|
|
account: 7,
|
|
|
|
xpub: 'tpubDDXFHr67Ro2tYpwnFJEQaM8eAPM2UV5uY6gFgXeSzS5aC5T9TfzXuawYKBbQMZJn8qHXLafY4tAutoda1aKP5h6Nbgy3swPbnhWbFjS5wnX',
|
|
|
|
}, {
|
|
|
|
purpose: 1017,
|
|
|
|
coin_type: 1,
|
|
|
|
account: 8,
|
|
|
|
xpub: 'tpubDDXFHr67Ro2tddKpAjUegXqt7EGxRXnHkeLbUkfuFMGbLJYgRpG4ew5pMmGg2nmcGmHFQ29w3juNhd8N5ZZ8HwJdymC4f5ukQLJ4yg9rEr3',
|
|
|
|
}, {
|
|
|
|
purpose: 1017,
|
|
|
|
coin_type: 1,
|
|
|
|
account: 9,
|
|
|
|
xpub: 'tpubDDXFHr67Ro2tgE89V8ZdgMytC2Jq1iT9ttGhdzR1X7haQJNBmXt8kau6taC6DGASYzbrjmo9z9w6JQFcaLNqbhS2h2PVSzKf79j265Zi8hF',
|
|
|
|
}]
|
|
|
|
}
|
|
|
|
}, (err, res) => {
|
|
|
|
if (err != null) {
|
|
|
|
console.log(err);
|
|
|
|
}
|
|
|
|
console.log(res);
|
|
|
|
});
|
|
|
|
```
|