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

Support remote signing over RPC #5689

Merged
merged 14 commits into from
Oct 19, 2021
Merged

Conversation

guggero
Copy link
Collaborator

@guggero guggero commented Sep 3, 2021

Fixes #5486 and #5779.
Also took some ideas from #3944, maybe we can close that as well.

Depends on #5708 and btcsuite/btcwallet#768.

This PR makes it possible to run an instance of lnd in purely watch-only mode (no private key material in the wallet whatsoever). All signing or ECDH requests are then forwarded to a secondary lnd instance (or, in theory, any software that implements the necessary RPC interfaces) for processing.

It is important that the secondary (the remote signing instance with the private) keys is NOT used as a wallet. The keys and addresses are kept in sync between the watch-only and the signing instance, so manually generating addresses or deriving keys on the signing instance will lead to unexpected errors on the watch-only instance!

@guggero guggero added rpc Related to the RPC interface wallet The wallet (lnwallet) which LND uses signrpc labels Sep 3, 2021
@guggero guggero added this to the v0.14.0 milestone Sep 3, 2021
@guggero guggero added this to In progress in v0.14.0-beta via automation Sep 3, 2021
@orijbot
Copy link

orijbot commented Sep 3, 2021

v0.14.0-beta automation moved this from In progress to Review in progress Sep 3, 2021
config.go Show resolved Hide resolved
Copy link
Member

@Roasbeef Roasbeef left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The final component falls into place! 🍾

Happy to see all the incremental steps we've taken over the past year or so resulted in this PR primary being some refactoring and additional layers of indirection.

Initial pass complete, main comment is the additional layer of indirection needed to allow the "primary" interfaces to be passed into the lnd.Main method rather than lnd initializing the concrete version of the interfaces within itself. Also see the msgs sent offline re some sample directions.

keychain/derivation.go Outdated Show resolved Hide resolved
keychain/derivation.go Outdated Show resolved Hide resolved
keychain/btcwallet.go Outdated Show resolved Hide resolved
keychain/btcwallet.go Outdated Show resolved Hide resolved
discovery/gossiper.go Outdated Show resolved Hide resolved
lnwallet/rpcwallet/rpcwallet.go Show resolved Hide resolved
lnrpc/walletrpc/walletkit.proto Show resolved Hide resolved
lnwallet/rpcwallet/rpcwallet.go Show resolved Hide resolved
lnwallet/rpcwallet/rpcwallet.go Outdated Show resolved Hide resolved
required by some hardware wallets for proper identification and signing. The
bytes must be in big-endian order.
*/
bytes master_key_fingerprint = 2;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a strong requirement that this is on this level? In that, can it go one layer below within the WatchOnlyAccount struct?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, you mean in case the accounts are not all derived from the same master key?

@guggero
Copy link
Collaborator Author

guggero commented Sep 10, 2021

I've extracted all the refactoring commits into #5708, will address the rest of the comments later.

@Roasbeef
Copy link
Member

So I think we want something like this:

diff --git a/keychain/btcwallet.go b/keychain/btcwallet.go
index 8d5dab3bd..3110fc960 100644
--- a/keychain/btcwallet.go
+++ b/keychain/btcwallet.go
@@ -64,7 +64,7 @@ type BtcWalletKeyRing struct {
 //
 // NOTE: The passed waddrmgr.Manager MUST be unlocked in order for the keychain
 // to function.
-func NewBtcWalletKeyRing(w *wallet.Wallet, coinType uint32) SecretKeyRing {
+func NewBtcWalletKeyRing(w *wallet.Wallet, coinType uint32) (SecretKeyRing, error) {
 	// Construct the key scope that will be used within the waddrmgr to
 	// create an HD chain for deriving all of our required keys. A different
 	// scope is used for each specific coin type.
@@ -73,10 +73,33 @@ func NewBtcWalletKeyRing(w *wallet.Wallet, coinType uint32) SecretKeyRing {
 		Coin:    coinType,
 	}
 
-	return &BtcWalletKeyRing{
+	// TODO(xxx): create all keyscopes here
+
+	wallet := &BtcWalletKeyRing{
 		wallet:        w,
 		chainKeyScope: chainKeyScope,
 	}
+
+	for _, keyFam := range VersionZeroKeyFamilies {
+		db := wallet.wallet.Database()
+		err := walletdb.Update(db, func(tx walletdb.ReadWriteTx) error {
+			addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey)
+
+			scope, err := wallet.keyScope()
+			if err != nil {
+				return err
+			}
+
+			// If the account doesn't exist, then we may need to create it
+			// for the first time in order to derive the keys that we
+			// require.
+			return wallet.createAccountIfNotExists(addrmgrNs, keyFam, scope)
+		})
+		if err != nil {
+			return nil, err
+		}
+	}
+	return wallet, nil
 }
 
 // keyScope attempts to return the key scope that we'll use to derive all of
diff --git a/keychain/derivation.go b/keychain/derivation.go
index b22d8f70f..15ac954e8 100644
--- a/keychain/derivation.go
+++ b/keychain/derivation.go
@@ -105,6 +105,21 @@ const (
 	KeyFamilyTowerID KeyFamily = 9
 )
 
+// VersionZeroKeyFamilies is a slice of all the known key families for first
+// version of the key derivation schema defined in this package.
+var VersionZeroKeyFamilies = []KeyFamily{
+	KeyFamilyMultiSig,
+	KeyFamilyRevocationBase,
+	KeyFamilyHtlcBase,
+	KeyFamilyPaymentBase,
+	KeyFamilyDelayBase,
+	KeyFamilyRevocationRoot,
+	KeyFamilyNodeKey,
+	KeyFamilyStaticBackup,
+	KeyFamilyTowerSession,
+	KeyFamilyTowerID,
+}
+
 // KeyLocator is a two-tuple that can be used to derive *any* key that has ever
 // been used under the key derivation mechanisms described in this file.
 // Version 0 of our key derivation schema uses the following BIP43-like

@Roasbeef
Copy link
Member

Roasbeef commented Sep 14, 2021

The other half (lets lncli wallet accounts list) display all the pubkeys:

diff --git a/lnwallet/btcwallet/btcwallet.go b/lnwallet/btcwallet/btcwallet.go
index 1db9e0b18..496ec1723 100644
--- a/lnwallet/btcwallet/btcwallet.go
+++ b/lnwallet/btcwallet/btcwallet.go
@@ -552,6 +552,18 @@ func (b *BtcWallet) ListAccounts(name string,
 			account := account
 			res = append(res, &account.AccountProperties)
 		}
+
+		accounts, err = b.wallet.Accounts(waddrmgr.KeyScope{
+			Purpose: keychain.BIP0043Purpose,
+			Coin:    b.cfg.CoinType,
+		})
+		if err != nil {
+			return nil, err
+		}
+		for _, account := range accounts.Accounts {
+			account := account
+			res = append(res, &account.AccountProperties)
+		}
 	}
 
 	return res, nil

@guggero
Copy link
Collaborator Author

guggero commented Sep 30, 2021

Rebased and updated with the above suggestions.
Now the only thing needed is a new command (perhaps lncli create-watchonly?) that takes the output of lncli wallet accounts list and converts it into the format that is needed for the InitWallet RPC.

@guggero guggero force-pushed the remote-signer branch 2 times, most recently from c710794 to 23fa101 Compare October 7, 2021 12:23
@guggero guggero changed the title WIP: Support remote signing over RPC Support remote signing over RPC Oct 7, 2021
@guggero guggero requested a review from Roasbeef October 7, 2021 19:35
@Roasbeef
Copy link
Member

Roasbeef commented Oct 7, 2021

Build fails rn:

watchtowerrpc -- unsafe]: exit status 1: go: github.com/btcsuite/btcwallet@v0.12.1-0.20210916213031-d0868cb9dd94 (replaced by ../../btcsuite/btcwallet): reading ../../btcsuite/btcwallet/go.mod: open /home/travis/gopath/src/github.com/btcsuite/btcwallet/go.mod: no such file or directory
go: github.com/btcsuite/btcwallet@v0.12.1-0.20210916213031-d0868cb9dd94 (replaced by ../../btcsuite/btcwallet): reading ../../btcsuite/btcwallet/go.mod: open /home/travis/gopath/src/github.com/btcsuite/btcwallet/go.mod: no such file or directory

Copy link
Member

@Roasbeef Roasbeef left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Getting close! Excellent set of docs also, looking forward to having this land in 01.4!

config_builder.go Show resolved Hide resolved
lnd.go Show resolved Hide resolved
return nil, fmt.Errorf("error connecting to the remote "+
"signing node through RPC: %v", err)
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing we may want to add here (doesn't necessarily need to be done here, can be follow up work) is a consistency check on start up. We can use lncli wallet accounts to see if all the key indexes line up, and advance them forward as needed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, definitely! Though this sounds like a good thing to split off into its own PR. Going to try and do this during the RC phase, or worst case for 0.14.1.

go.mod Outdated Show resolved Hide resolved
feeRate chainfee.SatPerKWeight, minConfs int32,
label string) (*wire.MsgTx, error) {

tx, err := r.WalletController.SendOutputs(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a dry run instance right?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we want to have this step? To check that it's indeed watch only?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, we want the watch-only wallet to actually lock the UTXOs and assume they're spent. Because we currently don't sync the UTXO locks, we need to make sure the watch-only wallet is in charge of doing coin selection and UTXO locking.
Or did you mean something else?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it. Didn't know the locking UTXO are wrapped inside SendOutputs.

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.admin.macaroon
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps instead guide the user to use a signer macaroon or a custom one that includes just the perms we need to interact w/ the remote signer?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great idea! I'm going to recommend the macaroon recipe for that in the docs.

the public node using NodeJS.

To use this example, first initialize the "signer" wallet with the root key
`tprv8ZgxMBicQKsPe6jS4vDm2n7s42Q6MpvghUQqMmSKG7bTZvGKtjrcU3PGzMNG37yzxywrcdvgkwrr8eYXJmbwdvUNVT4Ucv7ris4jvA7BUmg`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not clear how the user is meant to do this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lncli create now has the option to import an xprv if you press x here:

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):

But I can make that more explicit in the docs.

docs/remote-signing.md Show resolved Hide resolved
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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we've implemented this now in another btcwallet fork?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no additional btcwallet fork. Because we create the wallet for the first time and start/sync to chain after importing the accounts, we actually do get a full rescan of all addresses. I tested this locally and even address with index 9 is being detected as being used.
But this will only work in this case where we import an account before the initial chain sync (where we also set recovery_window). Importing an account later will not cause a rescan.
I think that's actually a nice thing to get an initial syncup when setting up the node pair.

client.initWallet({
wallet_password: Buffer.from(WATCH_ONLY_WALLET_PASSWORD, 'utf-8'),
recovery_window: 2500,
watch_only: {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment here re not hard coding (can get from the RPC?)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, we could do that. But it's just an example script to show what content the call needs to have. Can also remove the example script if it's more confusing than helping.

@guggero guggero force-pushed the remote-signer branch 3 times, most recently from c8b0c70 to 78f616b Compare October 11, 2021 09:53
Copy link
Collaborator

@yyforyongyu yyforyongyu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fantastic feature! And love to see the great documentation! I think it's looking gooood, a major question is do we plan to patch unit tests for those newly added functions? Plus few questions in the comments.

config_builder.go Show resolved Hide resolved
*walletConfig, partialChainControl.Cfg.BlockCache,
)
if err != nil {
fmt.Printf("unable to create wallet controller: %v\n", err)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leftover? Do we want to print to console?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just mirroring the code style in the rest of the file. We should probably clean that up in another PR.

// DefaultWalletImpl is the embedded instance of the default
// implementation that the remote signer uses as its watch-only wallet
// for keeping track of addresses and UTXOs.
*DefaultWalletImpl
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we embed by pointer here?

lnwallet/rpcwallet/rpcwallet.go Show resolved Hide resolved
// have all keys we have locally. Only if the remote wallet falls behind
// the local we might have problems that the remote wallet won't know
// outputs we're giving it to sign.
if uint32(remoteDesc.KeyLoc.KeyIndex) < localDesc.Index {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think they must be equal if using the same key locator?

},
}

if keyDesc.Index == 0 && keyDesc.PubKey != nil {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: might add some docs explaining why this check.

lnwallet/rpcwallet/rpcwallet.go Show resolved Hide resolved
feeRate chainfee.SatPerKWeight, minConfs int32,
label string) (*wire.MsgTx, error) {

tx, err := r.WalletController.SendOutputs(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we want to have this step? To check that it's indeed watch only?

// for it. Since lnd only uses SegWit addresses, we pick the
// date of the first block that contained SegWit transactions
// (481824).
initMsg.WatchOnlyBirthday = time.Date(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we use birthday to locate a birthdayBlock when we do syncWithChain inside btcwallet.

lnrpc/walletrpc/walletkit_util.go Show resolved Hide resolved
To make it possible to supply our own implementation of a secret key
ring, we extract that part from the chain control and split the whole
chain control creation into two parts.
In order to support the full range of on-chain functionality, including
importing watch-only accounts in the watch-only instance, we need to
forward some calls like creating new addresses or importing accounts to
the remote signing instance.
Copy link
Member

@Roasbeef Roasbeef left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 🍣

@@ -327,6 +327,7 @@ func main() {
}
app.Commands = []cli.Command{
createCommand,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might move this to reside under lncli wallet in a follow up change.

@Roasbeef Roasbeef merged commit 94b7b71 into lightningnetwork:master Oct 19, 2021
v0.14.0-beta automation moved this from Review in progress to Done Oct 19, 2021
@guggero guggero deleted the remote-signer branch October 20, 2021 11:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
rpc Related to the RPC interface signrpc wallet The wallet (lnwallet) which LND uses
Projects
No open projects
4 participants