-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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
Conversation
Visit https://dashboard.github.orijtech.com?back=0&pr=5689&remote=true&repo=guggero%2Flnd to see benchmark details. |
There was a problem hiding this 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.
required by some hardware wallets for proper identification and signing. The | ||
bytes must be in big-endian order. | ||
*/ | ||
bytes master_key_fingerprint = 2; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
7b46ef7
to
9370f8c
Compare
I've extracted all the refactoring commits into #5708, will address the rest of the comments later. |
d8f03d2
to
c8ed48e
Compare
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
|
The other half (lets 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 |
b95e738
to
0aaaf83
Compare
0aaaf83
to
1a55f90
Compare
Rebased and updated with the above suggestions. |
c710794
to
23fa101
Compare
Build fails rn:
|
There was a problem hiding this 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!
return nil, fmt.Errorf("error connecting to the remote "+ | ||
"signing node through RPC: %v", err) | ||
} | ||
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
feeRate chainfee.SatPerKWeight, minConfs int32, | ||
label string) (*wire.MsgTx, error) { | ||
|
||
tx, err := r.WalletController.SendOutputs( |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
.
docs/remote-signing.md
Outdated
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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` |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
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): |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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: { |
There was a problem hiding this comment.
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?)
There was a problem hiding this comment.
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.
c8b0c70
to
78f616b
Compare
There was a problem hiding this 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.
*walletConfig, partialChainControl.Cfg.BlockCache, | ||
) | ||
if err != nil { | ||
fmt.Printf("unable to create wallet controller: %v\n", err) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
// 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 { |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
feeRate chainfee.SatPerKWeight, minConfs int32, | ||
label string) (*wire.MsgTx, error) { | ||
|
||
tx, err := r.WalletController.SendOutputs( |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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.
78f616b
to
cc46a19
Compare
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.
cc46a19
to
0195196
Compare
0195196
to
d43854a
Compare
There was a problem hiding this 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, |
There was a problem hiding this comment.
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.
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 secondarylnd
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!