multi: forward address and import calls over RPC

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.
This commit is contained in:
Oliver Gugger committed 2021-10-14 15:42:51 +02:00
1 parent 9cae7ad3c2
commit 19db382e24
6 files changed
+703 -441

No files matched your search

+217 -8
View File
@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"crypto/x509"
"encoding/binary"
"errors"
"fmt"
"io/ioutil"
@@ -11,11 +12,15 @@ import (
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcutil/hdkeychain"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/lnrpc/signrpc"
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/macaroons"
"google.golang.org/grpc"
@@ -41,7 +46,12 @@ var (
// local watch-only wallet for keeping track of addresses and transactions but
// delegates any signing or ECDH operations to a remote node through RPC.
type RPCKeyRing struct {
watchOnlyWallet keychain.SecretKeyRing
// WalletController is the embedded wallet controller of the watch-only
// base wallet. We need to overwrite/shadow certain of the implemented
// methods to make sure we can mirror them to the remote wallet.
lnwallet.WalletController
watchOnlyKeyRing keychain.SecretKeyRing
rpcTimeout time.Duration
@@ -52,11 +62,13 @@ type RPCKeyRing struct {
var _ keychain.SecretKeyRing = (*RPCKeyRing)(nil)
var _ input.Signer = (*RPCKeyRing)(nil)
var _ keychain.MessageSignerRing = (*RPCKeyRing)(nil)
var _ lnwallet.WalletController = (*RPCKeyRing)(nil)
// NewRPCKeyRing creates a new remote signing secret key ring that uses the
// given watch-only base wallet to keep track of addresses and transactions but
// delegates any signing or ECDH operations to the remove signer through RPC.
func NewRPCKeyRing(watchOnlyWallet keychain.SecretKeyRing,
func NewRPCKeyRing(watchOnlyKeyRing keychain.SecretKeyRing,
watchOnlyWalletController lnwallet.WalletController,
remoteSigner *lncfg.RemoteSigner,
rpcTimeout time.Duration) (*RPCKeyRing, error) {
@@ -70,13 +82,189 @@ func NewRPCKeyRing(watchOnlyWallet keychain.SecretKeyRing,
}
return &RPCKeyRing{
watchOnlyWallet: watchOnlyWallet,
rpcTimeout: rpcTimeout,
signerClient: signrpc.NewSignerClient(rpcConn),
walletClient: walletrpc.NewWalletKitClient(rpcConn),
WalletController: watchOnlyWalletController,
watchOnlyKeyRing: watchOnlyKeyRing,
rpcTimeout: rpcTimeout,
signerClient: signrpc.NewSignerClient(rpcConn),
walletClient: walletrpc.NewWalletKitClient(rpcConn),
}, nil
}
// NewAddress returns the next external or internal address for the
// wallet dictated by the value of the `change` parameter. If change is
// true, then an internal address should be used, otherwise an external
// address should be returned. The type of address returned is dictated
// by the wallet's capabilities, and may be of type: p2sh, p2wkh,
// p2wsh, etc. The account parameter must be non-empty as it determines
// which account the address should be generated from.
func (r *RPCKeyRing) NewAddress(addrType lnwallet.AddressType, change bool,
account string) (btcutil.Address, error) {
ctxt, cancel := context.WithTimeout(context.Background(), r.rpcTimeout)
defer cancel()
rpcAddrType := walletrpc.AddressType_WITNESS_PUBKEY_HASH
if addrType == lnwallet.NestedWitnessPubKey {
rpcAddrType = walletrpc.AddressType_NESTED_WITNESS_PUBKEY_HASH
}
remoteAddr, err := r.walletClient.NextAddr(ctxt, &walletrpc.AddrRequest{
Account: account,
Type: rpcAddrType,
Change: change,
})
if err != nil {
return nil, fmt.Errorf("error deriving address on remote "+
"signer instance: %v", err)
}
localAddr, err := r.WalletController.NewAddress(
addrType, change, account,
)
if err != nil {
return nil, fmt.Errorf("error deriving address on local "+
"wallet instance: %v", err)
}
// We need to make sure we've derived the same address on the remote
// signing machine, otherwise we don't know whether we're at the same
// address index (and therefore the same wallet state in general).
if localAddr.String() != remoteAddr.Addr {
return nil, fmt.Errorf("error deriving address on remote "+
"signing instance, got different address (%s) than "+
"on local wallet instance (%s)", remoteAddr.Addr,
localAddr.String())
}
return localAddr, nil
}
// ImportAccount imports an account backed by an account extended public key.
// The master key fingerprint denotes the fingerprint of the root key
// corresponding to the account public key (also known as the key with
// derivation path m/). This may be required by some hardware wallets for proper
// identification and signing.
//
// The address type can usually be inferred from the key's version, but may be
// required for certain keys to map them into the proper scope.
//
// For BIP-0044 keys, an address type must be specified as we intend to not
// support importing BIP-0044 keys into the wallet using the legacy
// pay-to-pubkey-hash (P2PKH) scheme. A nested witness address type will force
// the standard BIP-0049 derivation scheme, while a witness address type will
// force the standard BIP-0084 derivation scheme.
//
// For BIP-0049 keys, an address type must also be specified to make a
// distinction between the standard BIP-0049 address schema (nested witness
// pubkeys everywhere) and our own BIP-0049Plus address schema (nested pubkeys
// externally, witness pubkeys internally).
func (r *RPCKeyRing) ImportAccount(name string,
accountPubKey *hdkeychain.ExtendedKey, masterKeyFingerprint uint32,
addrType *waddrmgr.AddressType,
dryRun bool) (*waddrmgr.AccountProperties, []btcutil.Address,
[]btcutil.Address, error) {
ctxt, cancel := context.WithTimeout(context.Background(), r.rpcTimeout)
defer cancel()
var masterKeyFingerprintBytes [4]byte
binary.BigEndian.PutUint32(
masterKeyFingerprintBytes[:], masterKeyFingerprint,
)
rpcAddrType, err := toRPCAddrType(addrType)
if err != nil {
return nil, nil, nil, fmt.Errorf("error converting address "+
"type: %v", err)
}
remoteAcct, err := r.walletClient.ImportAccount(
ctxt, &walletrpc.ImportAccountRequest{
Name: name,
ExtendedPublicKey: accountPubKey.String(),
MasterKeyFingerprint: masterKeyFingerprintBytes[:],
AddressType: rpcAddrType,
DryRun: dryRun,
},
)
if err != nil {
return nil, nil, nil, fmt.Errorf("error importing account on "+
"remote signer instance: %v", err)
}
props, extAddrs, intAddrs, err := r.WalletController.ImportAccount(
name, accountPubKey, masterKeyFingerprint, addrType, dryRun,
)
if err != nil {
return nil, nil, nil, fmt.Errorf("error importing account on "+
"local wallet instance: %v", err)
}
mismatchErr := fmt.Errorf("error importing account on remote signing "+
"instance, got different external addresses (%v) than on "+
"local wallet instance (%s)", remoteAcct.DryRunExternalAddrs,
extAddrs)
if len(remoteAcct.DryRunExternalAddrs) != len(extAddrs) {
return nil, nil, nil, mismatchErr
}
for idx, remoteExtAddr := range remoteAcct.DryRunExternalAddrs {
if extAddrs[idx].String() != remoteExtAddr {
return nil, nil, nil, mismatchErr
}
}
mismatchErr = fmt.Errorf("error importing account on remote signing "+
"instance, got different internal addresses (%v) than on "+
"local wallet instance (%s)", remoteAcct.DryRunInternalAddrs,
intAddrs)
if len(remoteAcct.DryRunInternalAddrs) != len(intAddrs) {
return nil, nil, nil, mismatchErr
}
for idx, remoteIntAddr := range remoteAcct.DryRunInternalAddrs {
if intAddrs[idx].String() != remoteIntAddr {
return nil, nil, nil, mismatchErr
}
}
return props, extAddrs, intAddrs, nil
}
// ImportPublicKey imports a single derived public key into the wallet. The
// address type can usually be inferred from the key's version, but in the case
// of legacy versions (xpub, tpub), an address type must be specified as we
// intend to not support importing BIP-44 keys into the wallet using the legacy
// pay-to-pubkey-hash (P2PKH) scheme.
func (r *RPCKeyRing) ImportPublicKey(pubKey *btcec.PublicKey,
addrType waddrmgr.AddressType) error {
ctxt, cancel := context.WithTimeout(context.Background(), r.rpcTimeout)
defer cancel()
rpcAddrType, err := toRPCAddrType(&addrType)
if err != nil {
return fmt.Errorf("error converting address type: %v", err)
}
_, err = r.walletClient.ImportPublicKey(
ctxt, &walletrpc.ImportPublicKeyRequest{
PublicKey: pubKey.SerializeCompressed(),
AddressType: rpcAddrType,
},
)
if err != nil {
return fmt.Errorf("error importing pubkey on remote signer "+
"instance: %v", err)
}
err = r.WalletController.ImportPublicKey(pubKey, addrType)
if err != nil {
return fmt.Errorf("error importing pubkey on local signer "+
"instance: %v", err)
}
return nil
}
// DeriveNextKey attempts to derive the *next* key within the key family
// (account in BIP43) specified. This method should return the next external
// child within this branch.
@@ -98,7 +286,7 @@ func (r *RPCKeyRing) DeriveNextKey(
"key on remote signer instance: %v", err)
}
localDesc, err := r.watchOnlyWallet.DeriveNextKey(keyFam)
localDesc, err := r.watchOnlyKeyRing.DeriveNextKey(keyFam)
if err != nil {
return keychain.KeyDescriptor{}, fmt.Errorf("error deriving "+
"key on local wallet instance: %v", err)
@@ -143,7 +331,7 @@ func (r *RPCKeyRing) DeriveKey(
"key on remote signer instance: %v", err)
}
localDesc, err := r.watchOnlyWallet.DeriveKey(keyLoc)
localDesc, err := r.watchOnlyKeyRing.DeriveKey(keyLoc)
if err != nil {
return keychain.KeyDescriptor{}, fmt.Errorf("error deriving "+
"key on local wallet instance: %v", err)
@@ -391,6 +579,27 @@ func toRPCSignReq(tx *wire.MsgTx,
}, nil
}
// toRPCAddrType converts the given address type to its RPC counterpart.
func toRPCAddrType(addrType *waddrmgr.AddressType) (walletrpc.AddressType,
error) {
if addrType == nil {
return walletrpc.AddressType_UNKNOWN, nil
}
switch *addrType {
case waddrmgr.WitnessPubKey:
return walletrpc.AddressType_WITNESS_PUBKEY_HASH, nil
case waddrmgr.NestedWitnessPubKey:
return walletrpc.AddressType_HYBRID_NESTED_WITNESS_PUBKEY_HASH,
nil
default:
return 0, fmt.Errorf("unhandled address type %v", *addrType)
}
}
// connectRPC tries to establish an RPC connection to the given host:port with
// the supplied certificate and macaroon.
func connectRPC(hostPort, tlsCertPath, macaroonPath string) (*grpc.ClientConn,