Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Outbound Remote Signer implementation #8754

Open
wants to merge 32 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
38cf24f
lnd+lncfg: add outbound remote signer to config
ViktorTigerstrom May 14, 2024
7b8d5de
lncfg: correct `DefaultRemoteSignerRPCTimeout` docs
ViktorTigerstrom May 20, 2024
fb31e03
lnd: add new `remotesigner` macaroon entity
ViktorTigerstrom Aug 23, 2024
e9a80bc
walletrpc: add `SignCoordinatorStreams` RPC
ViktorTigerstrom May 14, 2024
2cd4868
rpcwallet: add `RemoteSigner` interface
ViktorTigerstrom May 14, 2024
6f494b8
rpcwallet: add InboundRemoteSigner implementation
ViktorTigerstrom May 14, 2024
6953613
rpcwallet: add `RemoteSignerBuilder`
ViktorTigerstrom May 14, 2024
1fcc9e5
rpcwallet: use `RemoteSigner` in RPCKeyRing
ViktorTigerstrom May 14, 2024
88d4a09
lnd+rpcwallet: use `RemoteSigner` for health check
ViktorTigerstrom May 14, 2024
fc234dd
rpcwallet: add `RemoteSignerClient` struct
ViktorTigerstrom May 14, 2024
bde43f7
f - rpcwallet: use GoroutineManager in remote signer signer client
ViktorTigerstrom Oct 31, 2024
8393812
rpcwallet: Add `RemoteSignerClientBuilder`
ViktorTigerstrom Sep 1, 2024
803f559
lnd: add RemoteSignerClient instance on startup
ViktorTigerstrom May 14, 2024
07eaed3
lncfg: enable remote signer signertype `signer`
ViktorTigerstrom May 14, 2024
dd46ca2
rpcwallet: add `SignCoordinator` struct
ViktorTigerstrom May 14, 2024
a92bc7c
rpcwallet: add OutboundRemoteSigner implementation
ViktorTigerstrom May 14, 2024
05d9719
lnrpc: add AllowRemoteSigner WalletState proto
ViktorTigerstrom May 14, 2024
5e0bde8
rpcperms: allow some RPCs before rpcActive state
ViktorTigerstrom May 14, 2024
9e1e89a
rpcperms: fix SetServerActive function docs typo
ViktorTigerstrom May 14, 2024
327a7e9
multi: start RpcServer before dependencies exist
ViktorTigerstrom May 14, 2024
6dce647
multi: add `RemoteSigner` to walletrpc config
ViktorTigerstrom May 14, 2024
ff90a06
walletrpc: implement `SignCoordinatorStreams` RPC
ViktorTigerstrom May 14, 2024
5717a38
multi: add RemoteSigner before other dependencies
ViktorTigerstrom May 28, 2024
bae1391
multi: add `ReadySignal` to `WalletController`
ViktorTigerstrom May 14, 2024
0351035
lnd: await remote signer connection on startup
ViktorTigerstrom May 28, 2024
4d3bc8e
multi: enable remote signer signertype `outbound`
ViktorTigerstrom May 14, 2024
fcff95d
docs: add outbound signer to remote signing docs
ViktorTigerstrom May 13, 2024
55c773d
docs: update release notes
ViktorTigerstrom Oct 31, 2024
c452abe
lntest: separate creation/start of watch-only node
ViktorTigerstrom May 14, 2024
d4fb2c4
itest: add outbound remote signer itest
ViktorTigerstrom May 14, 2024
544e7f4
itest: add testOutboundRSMacaroonEnforcement itest
ViktorTigerstrom Aug 28, 2024
ae9a575
itest: wrap deriveCustomScopeAccounts at 80 chars
ViktorTigerstrom May 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion config.go
Original file line number Diff line number Diff line change
Expand Up @@ -722,7 +722,9 @@ func DefaultConfig() Config {
CoinSelectionStrategy: defaultCoinSelectionStrategy,
KeepFailedPaymentAttempts: defaultKeepFailedPaymentAttempts,
RemoteSigner: &lncfg.RemoteSigner{
Timeout: lncfg.DefaultRemoteSignerRPCTimeout,
SignerType: lncfg.DefaultInboundRemoteSignerType,
Timeout: lncfg.DefaultRemoteSignerRPCTimeout,
RequestTimeout: lncfg.DefaultRequestTimeout,
},
Sweeper: lncfg.DefaultSweeperConfig(),
Htlcswitch: &lncfg.Htlcswitch{
Expand Down
45 changes: 40 additions & 5 deletions config_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -843,28 +843,60 @@ func (d *RPCSignerWalletImpl) BuildChainControl(
partialChainControl *chainreg.PartialChainControl,
walletConfig *btcwallet.Config) (*chainreg.ChainControl, func(), error) {

// Keeps track of both the remote signer and the chain control clean up
// functions.
var (
cleanUpTasks []func()
cleanUp = func() {
for _, fn := range cleanUpTasks {
if fn == nil {
Roasbeef marked this conversation as resolved.
Show resolved Hide resolved
continue
}

fn()
}
}
)

walletController, err := btcwallet.New(
*walletConfig, partialChainControl.Cfg.BlockCache,
)
if err != nil {
err := fmt.Errorf("unable to create wallet controller: %w", err)
d.logger.Error(err)
return nil, nil, err
return nil, cleanUp, err
}

remoteSignerBuilder := rpcwallet.NewRemoteSignerBuilder(
d.DefaultWalletImpl.cfg.RemoteSigner,
)

// Create the remote signer instance. The remote signer instance type
// will depend on the configuration passed to the builders contructor.
remoteSigner, rsCleanUp, err := remoteSignerBuilder.Build()
if err != nil {
err := fmt.Errorf("unable to set up remote signer: %w", err)
d.logger.Error(err)

return nil, cleanUp, err
}

cleanUpTasks = append(cleanUpTasks, rsCleanUp)

baseKeyRing := keychain.NewBtcWalletKeyRing(
walletController.InternalWallet(), walletConfig.CoinType,
)

rpcKeyRing, err := rpcwallet.NewRPCKeyRing(
baseKeyRing, walletController,
d.DefaultWalletImpl.cfg.RemoteSigner, walletConfig.NetParams,
remoteSigner, walletConfig.NetParams,
)
if err != nil {
err := fmt.Errorf("unable to create RPC remote signing wallet "+
"%v", err)
d.logger.Error(err)
return nil, nil, err

return nil, cleanUp, err
}

// Create, and start the lnwallet, which handles the core payment
Expand All @@ -883,15 +915,18 @@ func (d *RPCSignerWalletImpl) BuildChainControl(

// We've created the wallet configuration now, so we can finish
// initializing the main chain control.
activeChainControl, cleanUp, err := chainreg.NewChainControl(
activeChainControl, ccCleanUp, err := chainreg.NewChainControl(
lnWalletConfig, rpcKeyRing, partialChainControl,
)
if err != nil {
err := fmt.Errorf("unable to create chain control: %w", err)
d.logger.Error(err)
return nil, nil, err

return nil, cleanUp, err
}

cleanUpTasks = append(cleanUpTasks, ccCleanUp)

return activeChainControl, cleanUp, nil
}

Expand Down
9 changes: 9 additions & 0 deletions docs/release-notes/release-notes-0.19.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@
`BumpForceCloseFee` which moves the functionality soley available in the
`lncli` to LND hence making it more universal.

* [SignCoordinatorStreams](https://github.com/lightningnetwork/lnd/pull/8754)
allows a remote signer to connect to the lnd node, if the
`remotesigner.signertype` cfg value has been set to `outbound`.

## lncli Additions

* [A pre-generated macaroon root key can now be specified in `lncli create` and
Expand All @@ -67,6 +71,11 @@

* LND updates channel.backup file at shutdown time.

* [Added](https://github.com/lightningnetwork/lnd/pull/8754) support for a new
remote signer type `outbound`, which makes an outbound connection to the
watch-only node, instead of requiring on an inbound connection from the
watch-only node.

## RPC Updates

## lncli Updates
Expand Down
200 changes: 188 additions & 12 deletions docs/remote-signing.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ keys in its wallet. The second instance (in this document referred to as
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
connection.
keys (the "signer") can be completely offline except for a single inbound or
outbound gRPC connection.
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.
network security, optimally only allowing the single gRPC connection to or from
the outside.

An example setup could look like:

Expand All @@ -39,12 +39,24 @@ xxx xx

```

## Example setup
When using a remote signer, the "signer" node can be configured to operate in
one of two modes.
It can either be configured as an "inbound" remote signer (the default setting)
or as an "outbound" remote signer. As an "inbound" remote signer, the signer
node permits a single inbound gRPC connection **from** the watch-only lnd node.
Conversely, when configured as an "outbound" remote signer, it allows a single
outbound gRPC connection **to** the watch-only lnd node.

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.
## Example setups

### The "signer" node
In the examples below, we demonstrate how to configure the "signer" node and the
"watch-only" node, when either using an "inbound" or an "outbound" remote
signer. The "signer" node possesses the full seed and private keys, while the
"watch-only" node holds only the public keys.

### Inbound remote signer example (default option)

#### The inbound "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
Expand Down Expand Up @@ -104,7 +116,7 @@ signer> $ lncli bakemacaroon --save_to signer.custom.macaroon \
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 "watch-only" node with an inbound remote signer

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
Expand All @@ -118,6 +130,9 @@ 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
# Optionally, specify that the watch-only node uses an inbound remote signer.
# However, since the default signertype is "inbound," this isn't required.
remotesigner.signertype=inbound
```

After starting "watch-only", the wallet can be created in watch-only mode by
Expand All @@ -136,7 +151,168 @@ 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.
through the RPC interface as is described in the
[section below](#Example-initialization-script).

### Outbound remote signer example

The setup of an outbound remote signer, can be done in 3 steps:

1. Start the signer node and export the `xpub`s of the wallet.
2. Bake a custom macaroon for the watch-only node with a specified root key,
which allows the signer node to establish an outbound connection to it.
3. Start watch-only node and initialize its watch-only wallet using the same
root key as in step 2.

Note: These steps are only required during the initial setup of the signer
wallet with a connected watch-only wallet. After this setup, the signer and
watch-only node can be started as usual, provided the configuration from these
steps remains in place.

#### Step 1: export the `xpub`s of the outbound signer node's wallet

When starting the signer node to export the `xpub`s of the wallet, these entries
in `lnd.conf` are recommended:

```text
# We apply some basic "hardening" parameters to make sure no connections to the
# internet are opened.

[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

# 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

# Specify that signer will make an outbound connection to the watch-only node.
remotesigner.signertype=signer

# The watch-only node's RPC host.
remotesigner.rpchost=zane.example.internal:10019

# A macaroon and TLS certificate for the watch-only node.
remotesigner.macaroonpath=/home/signer/example/watch-only.custom.macaroon
remotesigner.tlscertpath=/home/signer/example/watch-only.tls.cert
```

**Note:** The watch-only node’s `rpchost`, `macaroonpath`, and `tlscertpath`
specified in the configuration will not resolve successfully until steps 2 and 3
are completed, as these files do not yet exist, and no node is currently running
at the specified `rpchost`.
The signer node will continuously attempt to establish a connection to the
watch-only node using these values until the connection is successful.
Consequently, the configuration values will resolve properly once steps 2 and 3
have been executed.

After successfully starting up the "signer", and either unlocking an existing or
creating a new wallet, 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 (see [required accounts](#required-accounts) ).

#### Step 2: Bake the watch-only node's custom macaroon with a specified root key

To bake the custom macaroon for the watch-only node before its wallet exists,
first generate a root key, which will be used both to bake the macaroon and to
create the watch-only node's wallet.

Generation of a root key is exemplified below:

```shell
watch-only> $ ROOT_KEY=$(cat /dev/urandom | head -c32 | xxd -p -c32)
```

Once the root key is ready, bake the custom macaroon with:

```shell
watch-only> $ lncli bakemacaroon --root_key $ROOT_KEY \
--save_to /home/signer/example/watch-only.custom.macaroon remotesigner:generate
```

**Note:** The `save_to` path should match the `remotesigner.macaroonpath`
specified in step 1. If the signer and watch-only nodes are on separate
environments, move the macaroon to the `remotesigner.macaroonpath` after baking
it instead.

Also note that the watch-only node does not need to be running to execute this
command.


#### Step 3: Start the Watch-Only Node and Initialize Its Watch-Only Wallet

When starting the watch-only node, ensure the following entries are set in
`lnd.conf`:

```text
# Enable the use of a remote signer.
remotesigner.enable=true

# Specify that an outbound remote signer is being used.
remotesigner.signertype=outbound
```

It is also recommended to set the following entry, which defines the duration
the watch-only node will wait for a connection from the signer node during
startup before timing out and shutting down:

```text
# Set the duration the watch-only node will wait for the signer node's
# connection at startup.
remotesigner.timeout=5m
```

Since the signer node set up in Step 1 increases the delay between connection
attempts slightly with each failed attempt, it may take some time before it
reconnects to the watch-only node after it has been started. Setting a high
value for this configuration field will help ensure that the watch-only node
does not time out when starting up.

After starting the watch-only node, you can create a new watch-only wallet by
following the example below:

```shell
watch-only> $ lncli createwatchonly --mac_root_key $ROOT_KEY \
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):
```

**Note:** This command should be executed in an environment where the
`$ROOT_KEY` environment variable, created in Step 2, is defined. When selecting
a wallet birthday UNIX timestamp, choose one that is as close as possible to the
wallet’s actual creation time to expedite the initial setup of the watch-only
wallet.

Finally, if the watch-only node and signer node are set up in different
environments, you will also need to copy the watch-only node's TLS certificate
and place it in the path specified for the `remotesigner.tlscertpath`
configuration field in Step 1.

## Migrating an existing setup to remote signing

Expand All @@ -146,9 +322,9 @@ 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).
following the steps the "signer" node examples above.
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
"watch-only" node examples above. 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.
Expand Down
10 changes: 9 additions & 1 deletion itest/list_on_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -536,7 +536,15 @@ var allTestCases = []*lntest.TestCase{
},
{
Name: "remote signer",
TestFunc: testRemoteSigner,
TestFunc: testInboundRemoteSigner,
},
{
Name: "outbound remote signer",
TestFunc: testOutboundRemoteSigner,
},
{
Name: "outbound remote signer macaroon enforcement",
TestFunc: testOutboundRSMacaroonEnforcement,
},
{
Name: "taproot coop close",
Expand Down
Loading
Loading