From 9da8333a6e655cac645cd40f0f5f89fca196ace4 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Wed, 5 Jan 2022 11:04:14 +0100 Subject: [PATCH 01/17] multi: fix formatting --- input/signer.go | 7 ++--- lnrpc/signrpc/signer_server.go | 3 ++- lntest/mock/walletcontroller.go | 47 ++++++++++++++++++--------------- lnwallet/btcwallet/signer.go | 4 ++- lnwallet/interface.go | 13 +++++---- 5 files changed, 43 insertions(+), 31 deletions(-) diff --git a/input/signer.go b/input/signer.go index 86622638e..5d6f4257b 100644 --- a/input/signer.go +++ b/input/signer.go @@ -14,8 +14,8 @@ type Signer interface { // according to the data within the passed SignDescriptor. // // NOTE: The resulting signature should be void of a sighash byte. - SignOutputRaw(tx *wire.MsgTx, - signDesc *SignDescriptor) (Signature, error) + SignOutputRaw(tx *wire.MsgTx, signDesc *SignDescriptor) (Signature, + error) // ComputeInputScript generates a complete InputIndex for the passed // transaction with the signature as defined within the passed @@ -26,7 +26,8 @@ type Signer interface { // NOTE: This method will ignore any tweak parameters set within the // passed SignDescriptor as it assumes a set of typical script // templates (p2wkh, np2wkh, etc). - ComputeInputScript(tx *wire.MsgTx, signDesc *SignDescriptor) (*Script, error) + ComputeInputScript(tx *wire.MsgTx, signDesc *SignDescriptor) (*Script, + error) } // Script represents any script inputs required to redeem a previous diff --git a/lnrpc/signrpc/signer_server.go b/lnrpc/signrpc/signer_server.go index 51c2e55ed..147e146f2 100644 --- a/lnrpc/signrpc/signer_server.go +++ b/lnrpc/signrpc/signer_server.go @@ -238,7 +238,8 @@ func (r *ServerShell) CreateSubServer(configRegistry lnrpc.SubServerConfigDispat // provides an invalid transaction, then we'll return with an error. // // NOTE: The resulting signature should be void of a sighash byte. -func (s *Server) SignOutputRaw(ctx context.Context, in *SignReq) (*SignResp, error) { +func (s *Server) SignOutputRaw(ctx context.Context, in *SignReq) (*SignResp, + error) { switch { // If the client doesn't specify a transaction, then there's nothing to diff --git a/lntest/mock/walletcontroller.go b/lntest/mock/walletcontroller.go index 959fb5382..7df080257 100644 --- a/lntest/mock/walletcontroller.go +++ b/lntest/mock/walletcontroller.go @@ -54,14 +54,15 @@ func (w *WalletController) FetchInputInfo( } // ConfirmedBalance currently returns dummy values. -func (w *WalletController) ConfirmedBalance(confs int32, - _ string) (btcutil.Amount, error) { +func (w *WalletController) ConfirmedBalance(int32, string) (btcutil.Amount, + error) { + return 0, nil } // NewAddress is called to get new addresses for delivery, change etc. -func (w *WalletController) NewAddress(addrType lnwallet.AddressType, - change bool, _ string) (btcutil.Address, error) { +func (w *WalletController) NewAddress(lnwallet.AddressType, bool, + string) (btcutil.Address, error) { addr, _ := btcutil.NewAddressPubKey( w.RootKey.PubKey().SerializeCompressed(), &chaincfg.MainNetParams, @@ -70,19 +71,21 @@ func (w *WalletController) NewAddress(addrType lnwallet.AddressType, } // LastUnusedAddress currently returns dummy values. -func (w *WalletController) LastUnusedAddress(addrType lnwallet.AddressType, - _ string) (btcutil.Address, error) { +func (w *WalletController) LastUnusedAddress(lnwallet.AddressType, + string) (btcutil.Address, error) { + return nil, nil } // IsOurAddress currently returns a dummy value. -func (w *WalletController) IsOurAddress(a btcutil.Address) bool { +func (w *WalletController) IsOurAddress(btcutil.Address) bool { return false } // ListAccounts currently returns a dummy value. -func (w *WalletController) ListAccounts(_ string, - _ *waddrmgr.KeyScope) ([]*waddrmgr.AccountProperties, error) { +func (w *WalletController) ListAccounts(string, + *waddrmgr.KeyScope) ([]*waddrmgr.AccountProperties, error) { + return nil, nil } @@ -90,33 +93,35 @@ func (w *WalletController) ListAccounts(_ string, func (w *WalletController) ImportAccount(string, *hdkeychain.ExtendedKey, uint32, *waddrmgr.AddressType, bool) (*waddrmgr.AccountProperties, []btcutil.Address, []btcutil.Address, error) { + return nil, nil, nil, nil } // ImportPublicKey currently returns a dummy value. func (w *WalletController) ImportPublicKey(*btcec.PublicKey, waddrmgr.AddressType) error { + return nil } // SendOutputs currently returns dummy values. -func (w *WalletController) SendOutputs(outputs []*wire.TxOut, - _ chainfee.SatPerKWeight, _ int32, _ string) (*wire.MsgTx, error) { +func (w *WalletController) SendOutputs([]*wire.TxOut, + chainfee.SatPerKWeight, int32, string) (*wire.MsgTx, error) { return nil, nil } // CreateSimpleTx currently returns dummy values. -func (w *WalletController) CreateSimpleTx(outputs []*wire.TxOut, - _ chainfee.SatPerKWeight, _ int32, _ bool) (*txauthor.AuthoredTx, error) { +func (w *WalletController) CreateSimpleTx([]*wire.TxOut, + chainfee.SatPerKWeight, int32, bool) (*txauthor.AuthoredTx, error) { return nil, nil } // ListUnspentWitness is called by the wallet when doing coin selection. We just // need one unspent for the funding transaction. -func (w *WalletController) ListUnspentWitness(minConfs, - maxConfs int32, _ string) ([]*lnwallet.Utxo, error) { +func (w *WalletController) ListUnspentWitness(int32, int32, + string) ([]*lnwallet.Utxo, error) { // If the mock already has a list of utxos, return it. if w.Utxos != nil { @@ -140,8 +145,8 @@ func (w *WalletController) ListUnspentWitness(minConfs, } // ListTransactionDetails currently returns dummy values. -func (w *WalletController) ListTransactionDetails(_, - _ int32, _ string) ([]*lnwallet.TransactionDetail, error) { +func (w *WalletController) ListTransactionDetails(int32, int32, + string) ([]*lnwallet.TransactionDetail, error) { return nil, nil } @@ -169,8 +174,8 @@ func (w *WalletController) ListLeasedOutputs() ([]*wtxmgr.LockedOutput, error) { } // FundPsbt currently does nothing. -func (w *WalletController) FundPsbt(_ *psbt.Packet, _ int32, - _ chainfee.SatPerKWeight, _ string) (int32, error) { +func (w *WalletController) FundPsbt(*psbt.Packet, int32, chainfee.SatPerKWeight, + string) (int32, error) { return 0, nil } @@ -187,8 +192,8 @@ func (w *WalletController) PublishTransaction(tx *wire.MsgTx, _ string) error { } // LabelTransaction currently does nothing. -func (w *WalletController) LabelTransaction(_ chainhash.Hash, _ string, - _ bool) error { +func (w *WalletController) LabelTransaction(chainhash.Hash, string, + bool) error { return nil } diff --git a/lnwallet/btcwallet/signer.go b/lnwallet/btcwallet/signer.go index 277f39132..a6a525e7d 100644 --- a/lnwallet/btcwallet/signer.go +++ b/lnwallet/btcwallet/signer.go @@ -66,7 +66,9 @@ func deriveFromKeyLoc(scopedMgr *waddrmgr.ScopedKeyManager, // deriveKeyByLocator attempts to derive a key stored in the wallet given a // valid key locator. -func (b *BtcWallet) deriveKeyByLocator(keyLoc keychain.KeyLocator) (*btcec.PrivateKey, error) { +func (b *BtcWallet) deriveKeyByLocator( + keyLoc keychain.KeyLocator) (*btcec.PrivateKey, error) { + // We'll assume the special lightning key scope in this case. scopedMgr, err := b.wallet.Manager.FetchScopedKeyManager( b.chainKeyScope, diff --git a/lnwallet/interface.go b/lnwallet/interface.go index 42c2f633a..c6531ebd9 100644 --- a/lnwallet/interface.go +++ b/lnwallet/interface.go @@ -168,7 +168,8 @@ type WalletController interface { // NOTE: Only witness outputs should be included in the computation of // the total spendable balance of the wallet. We require this as only // witness inputs can be used for funding channels. - ConfirmedBalance(confs int32, accountFilter string) (btcutil.Amount, error) + ConfirmedBalance(confs int32, accountFilter string) (btcutil.Amount, + error) // NewAddress returns the next external or internal address for the // wallet dictated by the value of the `change` parameter. If change is @@ -197,7 +198,8 @@ type WalletController interface { // ListAccounts retrieves all accounts belonging to the wallet by // default. A name and key scope filter can be provided to filter // through all of the wallet accounts and return only those matching. - ListAccounts(string, *waddrmgr.KeyScope) ([]*waddrmgr.AccountProperties, error) + ListAccounts(string, *waddrmgr.KeyScope) ([]*waddrmgr.AccountProperties, + error) // ImportAccount imports an account backed by an account extended public // key. The master key fingerprint denotes the fingerprint of the root @@ -228,7 +230,8 @@ type WalletController interface { // 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. - ImportPublicKey(pubKey *btcec.PublicKey, addrType waddrmgr.AddressType) error + ImportPublicKey(pubKey *btcec.PublicKey, + addrType waddrmgr.AddressType) error // SendOutputs funds, signs, and broadcasts a Bitcoin transaction paying // out to the specified outputs. In the case the wallet has insufficient @@ -237,8 +240,8 @@ type WalletController interface { // be used when crafting the transaction. // // NOTE: This method requires the global coin selection lock to be held. - SendOutputs(outputs []*wire.TxOut, - feeRate chainfee.SatPerKWeight, minConfs int32, label string) (*wire.MsgTx, error) + SendOutputs(outputs []*wire.TxOut, feeRate chainfee.SatPerKWeight, + minConfs int32, label string) (*wire.MsgTx, error) // CreateSimpleTx creates a Bitcoin transaction paying to the specified // outputs. The transaction is not broadcasted to the network. In the From 1902ad724e64ba44d73e8bf4f1c1f557e96b24c4 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Wed, 5 Jan 2022 11:04:15 +0100 Subject: [PATCH 02/17] signrpc: remove incomplete sentence and TODO We use SignOutputRaw which expects a witness script being set, even for P2WKH. There is a special case in SignOutputRaw for the case where the script is a p2wkh script, then the input script is reconstructed correctly for the sighash. --- lnrpc/signrpc/signer_server.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lnrpc/signrpc/signer_server.go b/lnrpc/signrpc/signer_server.go index 147e146f2..c1b8e96d3 100644 --- a/lnrpc/signrpc/signer_server.go +++ b/lnrpc/signrpc/signer_server.go @@ -310,9 +310,12 @@ func (s *Server) SignOutputRaw(ctx context.Context, in *SignReq) (*SignResp, // If a witness script isn't passed, then we can't proceed, as // in the p2wsh case, we can't properly generate the sighash. + // A P2WKH doesn't need a witness script. But SignOutputRaw + // still needs to know the PK script that was used for the + // output. We'll send it in the WitnessScript field, the + // SignOutputRaw RPC will know what to do with it when creating + // the sighash. if len(signDesc.WitnessScript) == 0 { - // TODO(roasbeef): if regualr p2wkh, then at times - // internally we allow script to go by return nil, fmt.Errorf("witness script MUST be " + "specified") } From 8c77829c46d7b6792a43b408d3d4e54005010642 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Wed, 5 Jan 2022 11:04:16 +0100 Subject: [PATCH 03/17] lnwallet+lntest+mod: bump btcwallet to export ScriptForOutput --- go.mod | 2 +- go.sum | 4 ++-- lntest/mock/walletcontroller.go | 9 +++++++++ lnwallet/btcwallet/signer.go | 9 +++++++++ lnwallet/interface.go | 6 ++++++ 5 files changed, 27 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 0af0bdadf..e39c81af5 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f github.com/btcsuite/btcutil v1.0.3-0.20210527170813-e2ba6805a890 github.com/btcsuite/btcutil/psbt v1.0.3-0.20210527170813-e2ba6805a890 - github.com/btcsuite/btcwallet v0.13.0 + github.com/btcsuite/btcwallet v0.13.1-0.20211201210108-79de92f527dc github.com/btcsuite/btcwallet/wallet/txauthor v1.1.0 github.com/btcsuite/btcwallet/wallet/txrules v1.1.0 github.com/btcsuite/btcwallet/walletdb v1.3.6-0.20210803004036-eebed51155ec diff --git a/go.sum b/go.sum index 413d72a42..ecdf732c6 100644 --- a/go.sum +++ b/go.sum @@ -97,8 +97,8 @@ github.com/btcsuite/btcutil v1.0.3-0.20210527170813-e2ba6805a890/go.mod h1:0DVlH github.com/btcsuite/btcutil/psbt v1.0.3-0.20201208143702-a53e38424cce/go.mod h1:LVveMu4VaNSkIRTZu2+ut0HDBRuYjqGocxDMNS1KuGQ= github.com/btcsuite/btcutil/psbt v1.0.3-0.20210527170813-e2ba6805a890 h1:0xUNvvwJ7RjzBs4nCF+YrK28S5P/b4uHkpPxY1ovGY4= github.com/btcsuite/btcutil/psbt v1.0.3-0.20210527170813-e2ba6805a890/go.mod h1:LVveMu4VaNSkIRTZu2+ut0HDBRuYjqGocxDMNS1KuGQ= -github.com/btcsuite/btcwallet v0.13.0 h1:gtLWwueRm27KQiHJpycybv3uMdK1eo87JexfTfzvEhk= -github.com/btcsuite/btcwallet v0.13.0/go.mod h1:iLN1lG1MW0eREm+SikmPO8AZPz5NglBTEK/ErqkjGpo= +github.com/btcsuite/btcwallet v0.13.1-0.20211201210108-79de92f527dc h1:lAbAEAp4eWvsSwJfcpdHXpKz78X2sVF9aDK4nJveXmY= +github.com/btcsuite/btcwallet v0.13.1-0.20211201210108-79de92f527dc/go.mod h1:iLN1lG1MW0eREm+SikmPO8AZPz5NglBTEK/ErqkjGpo= github.com/btcsuite/btcwallet/wallet/txauthor v1.0.0/go.mod h1:VufDts7bd/zs3GV13f/lXc/0lXrPnvxD/NvmpG/FEKU= github.com/btcsuite/btcwallet/wallet/txauthor v1.1.0 h1:8pO0pvPX1rFRfRiol4oV6kX7dY5y4chPwhfVwUfvwtk= github.com/btcsuite/btcwallet/wallet/txauthor v1.1.0/go.mod h1:ktYuJyumYtwG+QQ832Q+kqvxWJRAei3Nqs5qhSn4nww= diff --git a/lntest/mock/walletcontroller.go b/lntest/mock/walletcontroller.go index 7df080257..72da14b14 100644 --- a/lntest/mock/walletcontroller.go +++ b/lntest/mock/walletcontroller.go @@ -53,6 +53,15 @@ func (w *WalletController) FetchInputInfo( return utxo, nil } +// ScriptForOutput returns the address, witness program and redeem script for a +// given UTXO. An error is returned if the UTXO does not belong to our wallet or +// it is not a managed pubKey address. +func (w *WalletController) ScriptForOutput(*wire.TxOut) ( + waddrmgr.ManagedPubKeyAddress, []byte, []byte, error) { + + return nil, nil, nil, nil +} + // ConfirmedBalance currently returns dummy values. func (w *WalletController) ConfirmedBalance(int32, string) (btcutil.Amount, error) { diff --git a/lnwallet/btcwallet/signer.go b/lnwallet/btcwallet/signer.go index a6a525e7d..cea9b977c 100644 --- a/lnwallet/btcwallet/signer.go +++ b/lnwallet/btcwallet/signer.go @@ -44,6 +44,15 @@ func (b *BtcWallet) FetchInputInfo(prevOut *wire.OutPoint) (*lnwallet.Utxo, erro }, nil } +// ScriptForOutput returns the address, witness program and redeem script for a +// given UTXO. An error is returned if the UTXO does not belong to our wallet or +// it is not a managed pubKey address. +func (b *BtcWallet) ScriptForOutput(output *wire.TxOut) ( + waddrmgr.ManagedPubKeyAddress, []byte, []byte, error) { + + return b.wallet.ScriptForOutput(output) +} + // deriveFromKeyLoc attempts to derive a private key using a fully specified // KeyLocator. func deriveFromKeyLoc(scopedMgr *waddrmgr.ScopedKeyManager, diff --git a/lnwallet/interface.go b/lnwallet/interface.go index c6531ebd9..a8e14dfff 100644 --- a/lnwallet/interface.go +++ b/lnwallet/interface.go @@ -158,6 +158,12 @@ type WalletController interface { // a non-nil error value of ErrNotMine should be returned instead. FetchInputInfo(prevOut *wire.OutPoint) (*Utxo, error) + // ScriptForOutput returns the address, witness program and redeem + // script for a given UTXO. An error is returned if the UTXO does not + // belong to our wallet or it is not a managed pubKey address. + ScriptForOutput(output *wire.TxOut) (waddrmgr.ManagedPubKeyAddress, + []byte, []byte, error) + // ConfirmedBalance returns the sum of all the wallet's unspent outputs // that have at least confs confirmations. If confs is set to zero, // then all unspent outputs, including those currently in the mempool From ca5d5023e32bf3be102c6a9f70acd740c36d3409 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Wed, 5 Jan 2022 11:04:18 +0100 Subject: [PATCH 04/17] btcwallet: move PSBT related methods to own file This is a pure code move commit to extract the FundPsbt and FinalizePsbt methods into their own file. --- lnwallet/btcwallet/btcwallet.go | 99 ----------------------------- lnwallet/btcwallet/psbt.go | 107 ++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 99 deletions(-) create mode 100644 lnwallet/btcwallet/psbt.go diff --git a/lnwallet/btcwallet/btcwallet.go b/lnwallet/btcwallet/btcwallet.go index 86793d182..bc4922eb6 100644 --- a/lnwallet/btcwallet/btcwallet.go +++ b/lnwallet/btcwallet/btcwallet.go @@ -16,7 +16,6 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil/hdkeychain" - "github.com/btcsuite/btcutil/psbt" "github.com/btcsuite/btcwallet/chain" "github.com/btcsuite/btcwallet/waddrmgr" "github.com/btcsuite/btcwallet/wallet" @@ -1114,104 +1113,6 @@ func (b *BtcWallet) ListTransactionDetails(startHeight, endHeight int32, return txDetails, nil } -// FundPsbt creates a fully populated PSBT packet that contains enough inputs to -// fund the outputs specified in the passed in packet with the specified fee -// rate. If there is change left, a change output from the internal wallet is -// added and the index of the change output is returned. Otherwise no additional -// output is created and the index -1 is returned. -// -// NOTE: If the packet doesn't contain any inputs, coin selection is performed -// automatically. The account parameter must be non-empty as it determines which -// set of coins are eligible for coin selection. If the packet does contain any -// inputs, it is assumed that full coin selection happened externally and no -// additional inputs are added. If the specified inputs aren't enough to fund -// the outputs with the given fee rate, an error is returned. No lock lease is -// acquired for any of the selected/validated inputs. It is in the caller's -// responsibility to lock the inputs before handing them out. -// -// This is a part of the WalletController interface. -func (b *BtcWallet) FundPsbt(packet *psbt.Packet, minConfs int32, - feeRate chainfee.SatPerKWeight, accountName string) (int32, error) { - - // The fee rate is passed in using units of sat/kw, so we'll convert - // this to sat/KB as the CreateSimpleTx method requires this unit. - feeSatPerKB := btcutil.Amount(feeRate.FeePerKVByte()) - - var ( - keyScope *waddrmgr.KeyScope - accountNum uint32 - ) - switch accountName { - // If the default/imported account name was specified, we'll provide a - // nil key scope to FundPsbt, allowing it to select inputs from both key - // scopes (NP2WKH, P2WKH). - case lnwallet.DefaultAccountName: - accountNum = defaultAccount - - case waddrmgr.ImportedAddrAccountName: - accountNum = importedAccount - - // Otherwise, map the account name to its key scope and internal account - // number to only select inputs from said account. - default: - scope, account, err := b.wallet.LookupAccount(accountName) - if err != nil { - return 0, err - } - keyScope = &scope - accountNum = account - } - - // Let the wallet handle coin selection and/or fee estimation based on - // the partial TX information in the packet. - return b.wallet.FundPsbt( - packet, keyScope, minConfs, accountNum, feeSatPerKB, - b.cfg.CoinSelectionStrategy, - ) -} - -// FinalizePsbt expects a partial transaction with all inputs and outputs fully -// declared and tries to sign all inputs that belong to the specified account. -// Lnd must be the last signer of the transaction. That means, if there are any -// unsigned non-witness inputs or inputs without UTXO information attached or -// inputs without witness data that do not belong to lnd's wallet, this method -// will fail. If no error is returned, the PSBT is ready to be extracted and the -// final TX within to be broadcast. -// -// NOTE: This method does NOT publish the transaction after it's been -// finalized successfully. -// -// This is a part of the WalletController interface. -func (b *BtcWallet) FinalizePsbt(packet *psbt.Packet, accountName string) error { - var ( - keyScope *waddrmgr.KeyScope - accountNum uint32 - ) - switch accountName { - // If the default/imported account name was specified, we'll provide a - // nil key scope to FundPsbt, allowing it to sign inputs from both key - // scopes (NP2WKH, P2WKH). - case lnwallet.DefaultAccountName: - accountNum = defaultAccount - - case waddrmgr.ImportedAddrAccountName: - accountNum = importedAccount - - // Otherwise, map the account name to its key scope and internal account - // number to determine if the inputs belonging to this account should be - // signed. - default: - scope, account, err := b.wallet.LookupAccount(accountName) - if err != nil { - return err - } - keyScope = &scope - accountNum = account - } - - return b.wallet.FinalizePsbt(keyScope, accountNum, packet) -} - // txSubscriptionClient encapsulates the transaction notification client from // the base wallet. Notifications received from the client will be proxied over // two distinct channels. diff --git a/lnwallet/btcwallet/psbt.go b/lnwallet/btcwallet/psbt.go new file mode 100644 index 000000000..8e657f0c5 --- /dev/null +++ b/lnwallet/btcwallet/psbt.go @@ -0,0 +1,107 @@ +package btcwallet + +import ( + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcutil/psbt" + "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/lightningnetwork/lnd/lnwallet" + "github.com/lightningnetwork/lnd/lnwallet/chainfee" +) + +// FundPsbt creates a fully populated PSBT packet that contains enough inputs to +// fund the outputs specified in the passed in packet with the specified fee +// rate. If there is change left, a change output from the internal wallet is +// added and the index of the change output is returned. Otherwise no additional +// output is created and the index -1 is returned. +// +// NOTE: If the packet doesn't contain any inputs, coin selection is performed +// automatically. The account parameter must be non-empty as it determines which +// set of coins are eligible for coin selection. If the packet does contain any +// inputs, it is assumed that full coin selection happened externally and no +// additional inputs are added. If the specified inputs aren't enough to fund +// the outputs with the given fee rate, an error is returned. No lock lease is +// acquired for any of the selected/validated inputs. It is in the caller's +// responsibility to lock the inputs before handing them out. +// +// This is a part of the WalletController interface. +func (b *BtcWallet) FundPsbt(packet *psbt.Packet, minConfs int32, + feeRate chainfee.SatPerKWeight, accountName string) (int32, error) { + + // The fee rate is passed in using units of sat/kw, so we'll convert + // this to sat/KB as the CreateSimpleTx method requires this unit. + feeSatPerKB := btcutil.Amount(feeRate.FeePerKVByte()) + + var ( + keyScope *waddrmgr.KeyScope + accountNum uint32 + ) + switch accountName { + // If the default/imported account name was specified, we'll provide a + // nil key scope to FundPsbt, allowing it to select inputs from both key + // scopes (NP2WKH, P2WKH). + case lnwallet.DefaultAccountName: + accountNum = defaultAccount + + case waddrmgr.ImportedAddrAccountName: + accountNum = importedAccount + + // Otherwise, map the account name to its key scope and internal account + // number to only select inputs from said account. + default: + scope, account, err := b.wallet.LookupAccount(accountName) + if err != nil { + return 0, err + } + keyScope = &scope + accountNum = account + } + + // Let the wallet handle coin selection and/or fee estimation based on + // the partial TX information in the packet. + return b.wallet.FundPsbt( + packet, keyScope, minConfs, accountNum, feeSatPerKB, + b.cfg.CoinSelectionStrategy, + ) +} + +// FinalizePsbt expects a partial transaction with all inputs and outputs fully +// declared and tries to sign all inputs that belong to the specified account. +// Lnd must be the last signer of the transaction. That means, if there are any +// unsigned non-witness inputs or inputs without UTXO information attached or +// inputs without witness data that do not belong to lnd's wallet, this method +// will fail. If no error is returned, the PSBT is ready to be extracted and the +// final TX within to be broadcast. +// +// NOTE: This method does NOT publish the transaction after it's been +// finalized successfully. +// +// This is a part of the WalletController interface. +func (b *BtcWallet) FinalizePsbt(packet *psbt.Packet, accountName string) error { + var ( + keyScope *waddrmgr.KeyScope + accountNum uint32 + ) + switch accountName { + // If the default/imported account name was specified, we'll provide a + // nil key scope to FundPsbt, allowing it to sign inputs from both key + // scopes (NP2WKH, P2WKH). + case lnwallet.DefaultAccountName: + accountNum = defaultAccount + + case waddrmgr.ImportedAddrAccountName: + accountNum = importedAccount + + // Otherwise, map the account name to its key scope and internal account + // number to determine if the inputs belonging to this account should be + // signed. + default: + scope, account, err := b.wallet.LookupAccount(accountName) + if err != nil { + return err + } + keyScope = &scope + accountNum = account + } + + return b.wallet.FinalizePsbt(keyScope, accountNum, packet) +} From 167a1f2b7977168f8ab833eafbb48919e22884e9 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Wed, 5 Jan 2022 11:04:19 +0100 Subject: [PATCH 05/17] btcwallet: add method for deriving key from BIP32 path --- lnwallet/btcwallet/signer.go | 130 ++++++++++++++++ lnwallet/btcwallet/signer_test.go | 241 ++++++++++++++++++++++++++++++ 2 files changed, 371 insertions(+) create mode 100644 lnwallet/btcwallet/signer_test.go diff --git a/lnwallet/btcwallet/signer.go b/lnwallet/btcwallet/signer.go index cea9b977c..bf339b519 100644 --- a/lnwallet/btcwallet/signer.go +++ b/lnwallet/btcwallet/signer.go @@ -1,11 +1,14 @@ package btcwallet import ( + "fmt" + "github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcutil/hdkeychain" "github.com/btcsuite/btcwallet/waddrmgr" "github.com/btcsuite/btcwallet/walletdb" "github.com/go-errors/errors" @@ -73,6 +76,133 @@ func deriveFromKeyLoc(scopedMgr *waddrmgr.ScopedKeyManager, return addr.(waddrmgr.ManagedPubKeyAddress).PrivKey() } +// deriveKeyByBIP32Path derives a key described by a BIP32 path. We expect the +// first three elements of the path to be hardened according to BIP44, so they +// must be a number >= 2^31. +func (b *BtcWallet) deriveKeyByBIP32Path(path []uint32) (*btcec.PrivateKey, + error) { + + // Make sure we get a full path with exactly 5 elements. A path is + // either custom purpose one with 4 dynamic and one static elements: + // m/1017'/coinType'/keyFamily'/0/index + // Or a default BIP49/89 one with 5 elements: + // m/purpose'/coinType'/account'/change/index + const expectedDerivationPathDepth = 5 + if len(path) != expectedDerivationPathDepth { + return nil, fmt.Errorf("invalid BIP32 derivation path, "+ + "expected path length %d, instead was %d", + expectedDerivationPathDepth, len(path)) + } + + // Assert that the first three parts of the path are actually hardened + // to avoid under-flowing the uint32 type. + if err := assertHardened(path[0], path[1], path[2]); err != nil { + return nil, fmt.Errorf("invalid BIP32 derivation path, "+ + "expected first three elements to be hardened: %w", err) + } + + purpose := path[0] - hdkeychain.HardenedKeyStart + coinType := path[1] - hdkeychain.HardenedKeyStart + account := path[2] - hdkeychain.HardenedKeyStart + change, index := path[3], path[4] + + // Is this a custom lnd internal purpose key? + switch purpose { + case keychain.BIP0043Purpose: + // Make sure it's for the same coin type as our wallet's + // keychain scope. + if coinType != b.chainKeyScope.Coin { + return nil, fmt.Errorf("invalid BIP32 derivation "+ + "path, expected coin type %d, instead was %d", + b.chainKeyScope.Coin, coinType) + } + + return b.deriveKeyByLocator(keychain.KeyLocator{ + Family: keychain.KeyFamily(account), + Index: index, + }) + + // Is it a standard, BIP defined purpose that the wallet understands? + case waddrmgr.KeyScopeBIP0044.Purpose, + waddrmgr.KeyScopeBIP0049Plus.Purpose, + waddrmgr.KeyScopeBIP0084.Purpose: + + // We're going to continue below the switch statement to avoid + // unnecessary indentation for this default case. + + // Currently, there is no way to import any other key scopes than the + // one custom purpose or three standard ones into lnd's wallet. So we + // shouldn't accept any other scopes to sign for. + default: + return nil, fmt.Errorf("invalid BIP32 derivation path, "+ + "unknown purpose %d", purpose) + } + + // Okay, we made sure it's a BIP49/84 key, so we need to derive it now. + // Interestingly, the btcwallet never actually uses a coin type other + // than 0 for those keys, so we need to make sure this behavior is + // replicated here. + if coinType != 0 { + return nil, fmt.Errorf("invalid BIP32 derivation path, coin " + + "type must be 0 for BIP49/84 btcwallet keys") + } + + // We only expect to be asked to sign with key scopes that we know + // about. So if the scope doesn't exist, we don't create it. + scope := waddrmgr.KeyScope{ + Purpose: purpose, + Coin: coinType, + } + scopedMgr, err := b.wallet.Manager.FetchScopedKeyManager(scope) + if err != nil { + return nil, fmt.Errorf("error fetching manager for scope %v: "+ + "%w", scope, err) + } + + // Let's see if we can hit the private key cache. + keyPath := waddrmgr.DerivationPath{ + InternalAccount: account, + Account: account, + Branch: change, + Index: index, + } + privKey, err := scopedMgr.DeriveFromKeyPathCache(keyPath) + if err == nil { + return privKey, nil + } + + // The key wasn't in the cache, let's fully derive it now. + err = walletdb.View(b.db, func(tx walletdb.ReadTx) error { + addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + + addr, err := scopedMgr.DeriveFromKeyPath(addrmgrNs, keyPath) + if err != nil { + return fmt.Errorf("error deriving private key: %w", err) + } + + privKey, err = addr.(waddrmgr.ManagedPubKeyAddress).PrivKey() + return err + }) + if err != nil { + return nil, fmt.Errorf("error deriving key from path %#v: %w", + keyPath, err) + } + + return privKey, nil +} + +// assertHardened makes sure each given element is >= 2^31. +func assertHardened(elements ...uint32) error { + for idx, element := range elements { + if element < hdkeychain.HardenedKeyStart { + return fmt.Errorf("element at index %d is not hardened", + idx) + } + } + + return nil +} + // deriveKeyByLocator attempts to derive a key stored in the wallet given a // valid key locator. func (b *BtcWallet) deriveKeyByLocator( diff --git a/lnwallet/btcwallet/signer_test.go b/lnwallet/btcwallet/signer_test.go new file mode 100644 index 000000000..be15acb4d --- /dev/null +++ b/lnwallet/btcwallet/signer_test.go @@ -0,0 +1,241 @@ +package btcwallet + +import ( + "encoding/hex" + "io/ioutil" + "os" + "testing" + "time" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/integration/rpctest" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcutil/hdkeychain" + "github.com/btcsuite/btcwallet/chain" + "github.com/lightningnetwork/lnd/blockcache" + "github.com/lightningnetwork/lnd/lnwallet" + "github.com/stretchr/testify/require" +) + +var ( + // seedBytes is the raw entropy of the aezeed: + // able promote dizzy mixture sword myth share public find tattoo + // catalog cousin bulb unfair machine alarm cool large promote kick + // shop rug mean year + // Which corresponds to the master root key: + // xprv9s21ZrQH143K2KADjED57FvNbptdKLp4sqKzssegwEGKQMGoDkbyhUeCKe5m3A + // MU44z4vqkmGswwQVKrv599nFG16PPZDEkNrogwoDGeCmZ + seedBytes, _ = hex.DecodeString("4a7611b6979ba7c4bc5c5cd2239b2973") + + // firstAddress is the first address that we should get from the wallet, + // corresponding to the derivation path m/84'/0'/0'/0/0 (even on regtest + // which is a special case for the BIP49/84 addresses in btcwallet). + firstAddress = "bcrt1qgdlgjc5ede7fjv350wcjqat80m0zsmfaswsj9p" + + testCases = []struct { + name string + path []uint32 + err string + wif string + }{{ + name: "m/84'/0'/0'/0/0", + path: []uint32{ + hardenedKey(84), hardenedKey(0), hardenedKey(0), 0, 0, + }, + wif: "cPp3XUewCBQVg3pgwVWbtpzDwhWTQpHhu8saN3SdGRTkiLpu1R6h", + }, { + name: "m/84'/0'/0'/1/0", + path: []uint32{ + hardenedKey(84), hardenedKey(0), hardenedKey(0), 1, 0, + }, + wif: "cPUR1nFAeYAtSWSkKoWB6WbzRTbDSGdrGRmv1kVLRPyo7QXph2gt", + }, { + name: "m/84'/0'/0'/0/12345", + path: []uint32{ + hardenedKey(84), hardenedKey(0), hardenedKey(0), 0, + 12345, + }, + wif: "cQCdGxqKeGZKiC2uRYMAGenJHkDvajiPieT4Yg7k1BKawjKkywvz", + }, { + name: "m/49'/0'/0'/0/0", + path: []uint32{ + hardenedKey(49), hardenedKey(0), hardenedKey(0), 0, 0, + }, + wif: "cMwVK2bcTzivPZfcCH585rBGghqsJAP9MdVy8inRti1wZvLn5DvY", + }, { + name: "m/49'/0'/0'/1/0", + path: []uint32{ + hardenedKey(49), hardenedKey(0), hardenedKey(0), 1, 0, + }, + wif: "cNPW9bMtdc2YGBzWzSCXFN4excjrT34nZzGYtfkzkazUrt3dXuv7", + }, { + name: "m/49'/0'/0'/1/12345", + path: []uint32{ + hardenedKey(49), hardenedKey(0), hardenedKey(0), 1, + 12345, + }, + wif: "cNdJt2fSNUJYVSb8JFjhosPcQgNvJ92SjNeNpsf1gUwDVDv2KVRa", + }, { + name: "m/1017'/1'/0'/0/0", + path: []uint32{ + hardenedKey(1017), hardenedKey(1), hardenedKey(0), 0, 0, + }, + wif: "cPsCmbWQENgptj3eTiyd85QSAD1xqYKPM9jUkfvm7vgN3SoVPWSP", + }, { + name: "m/1017'/1'/6'/0/0", + path: []uint32{ + hardenedKey(1017), hardenedKey(1), hardenedKey(6), 0, 0, + }, + wif: "cPeQdpcGJmLqpdmvokh3DK9ZtjYAXxiw4p4ELNUWkWt6bMRqArEV", + }, { + name: "m/1017'/1'/7'/0/123", + path: []uint32{ + hardenedKey(1017), hardenedKey(1), hardenedKey(7), 0, + 123, + }, + wif: "cPcWZMqY4YErkcwjtFJaYoXkzd7bKxrfxAVzhDgy3n5BGH8CU8sn", + }, { + name: "m/84'/1'/0'/0/0", + path: []uint32{ + hardenedKey(84), hardenedKey(1), hardenedKey(0), 0, 0, + }, + err: "coin type must be 0 for BIP49/84 btcwallet keys", + }, { + name: "m/1017'/0'/0'/0/0", + path: []uint32{ + hardenedKey(1017), hardenedKey(0), hardenedKey(0), 0, 0, + }, + err: "expected coin type 1, instead was 0", + }, { + name: "m/84'/0'/1'/0/0", + path: []uint32{ + hardenedKey(84), hardenedKey(0), hardenedKey(1), 0, 0, + }, + err: "account 1 not found", + }, { + name: "m/49'/0'/1'/0/0", + path: []uint32{ + hardenedKey(49), hardenedKey(0), hardenedKey(1), 0, 0, + }, + err: "account 1 not found", + }, { + name: "non-hardened purpose m/84/0/0/0/0", + path: []uint32{84, 0, 0, 0, 0}, + err: "element at index 0 is not hardened", + }, { + name: "non-hardened account m/84'/0'/0/0/0", + path: []uint32{hardenedKey(84), hardenedKey(0), 0, 0, 0}, + err: "element at index 2 is not hardened", + }} +) + +// TestBip32KeyDerivation makes sure that private keys can be derived from a +// BIP32 key path correctly. +func TestBip32KeyDerivation(t *testing.T) { + netParams := &chaincfg.RegressionNetParams + w, cleanup := newTestWallet(t, netParams, seedBytes) + defer cleanup() + + // This is just a sanity check that the wallet was initialized + // correctly. We make sure the first derived address is the expected + // one. + firstDerivedAddr, err := w.NewAddress( + lnwallet.WitnessPubKey, false, lnwallet.DefaultAccountName, + ) + require.NoError(t, err) + require.Equal(t, firstAddress, firstDerivedAddr.String()) + + // Let's go through the test cases now that we know our wallet is ready. + for _, tc := range testCases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + privKey, err := w.deriveKeyByBIP32Path(tc.path) + + if tc.err == "" { + require.NoError(t, err) + wif, err := btcutil.NewWIF( + privKey, netParams, true, + ) + require.NoError(t, err) + require.Equal(t, tc.wif, wif.String()) + } else { + require.Error(t, err) + require.Contains(t, err.Error(), tc.err) + } + }) + } +} + +func newTestWallet(t *testing.T, netParams *chaincfg.Params, + seedBytes []byte) (*BtcWallet, func()) { + + tempDir, err := ioutil.TempDir("", "lnwallet") + if err != nil { + _ = os.RemoveAll(tempDir) + t.Fatalf("creating temp dir failed: %v", err) + } + + chainBackend, backendCleanup := getChainBackend(t, netParams) + cleanup := func() { + _ = os.RemoveAll(tempDir) + backendCleanup() + } + + loaderOpt := LoaderWithLocalWalletDB(tempDir, false, time.Minute) + config := Config{ + PrivatePass: []byte("some-pass"), + HdSeed: seedBytes, + NetParams: netParams, + CoinType: netParams.HDCoinType, + ChainSource: chainBackend, + // wallet starts in recovery mode + RecoveryWindow: 2, + LoaderOptions: []LoaderOption{loaderOpt}, + } + blockCache := blockcache.NewBlockCache(10000) + w, err := New(config, blockCache) + if err != nil { + cleanup() + t.Fatalf("creating wallet failed: %v", err) + } + + err = w.Start() + if err != nil { + cleanup() + t.Fatalf("starting wallet failed: %v", err) + } + + return w, cleanup +} + +// getChainBackend returns a simple btcd based chain backend to back the wallet. +func getChainBackend(t *testing.T, netParams *chaincfg.Params) (chain.Interface, + func()) { + + miningNode, err := rpctest.New(netParams, nil, nil, "") + require.NoError(t, err) + require.NoError(t, miningNode.SetUp(true, 25)) + + // Next, mine enough blocks in order for SegWit and the CSV package + // soft-fork to activate on RegNet. + numBlocks := netParams.MinerConfirmationWindow * 2 + _, err = miningNode.Client.Generate(numBlocks) + require.NoError(t, err) + + rpcConfig := miningNode.RPCConfig() + chainClient, err := chain.NewRPCClient( + netParams, rpcConfig.Host, rpcConfig.User, rpcConfig.Pass, + rpcConfig.Certificates, false, 20, + ) + require.NoError(t, err) + + return chainClient, func() { + _ = miningNode.TearDown() + } +} + +// hardenedKey returns a key of a hardened derivation key path. +func hardenedKey(part uint32) uint32 { + return part + hdkeychain.HardenedKeyStart +} From b54279dd8747746c654242014f8da3e82a0971a3 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Wed, 5 Jan 2022 11:04:20 +0100 Subject: [PATCH 06/17] lnwallet: add derivation and prev TX to UTXO --- lnwallet/btcwallet/signer.go | 6 +++++- lnwallet/interface.go | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/lnwallet/btcwallet/signer.go b/lnwallet/btcwallet/signer.go index bf339b519..8ecaf5cd8 100644 --- a/lnwallet/btcwallet/signer.go +++ b/lnwallet/btcwallet/signer.go @@ -24,7 +24,9 @@ import ( // // This is a part of the WalletController interface. func (b *BtcWallet) FetchInputInfo(prevOut *wire.OutPoint) (*lnwallet.Utxo, error) { - _, txOut, _, confirmations, err := b.wallet.FetchInputInfo(prevOut) + prevTx, txOut, bip32, confirmations, err := b.wallet.FetchInputInfo( + prevOut, + ) if err != nil { return nil, err } @@ -44,6 +46,8 @@ func (b *BtcWallet) FetchInputInfo(prevOut *wire.OutPoint) (*lnwallet.Utxo, erro PkScript: txOut.PkScript, Confirmations: confirmations, OutPoint: *prevOut, + Derivation: bip32, + PrevTx: prevTx, }, nil } diff --git a/lnwallet/interface.go b/lnwallet/interface.go index a8e14dfff..455d48b00 100644 --- a/lnwallet/interface.go +++ b/lnwallet/interface.go @@ -78,6 +78,8 @@ type Utxo struct { Confirmations int64 PkScript []byte wire.OutPoint + Derivation *psbt.Bip32Derivation + PrevTx *wire.MsgTx } // TransactionDetail describes a transaction with either inputs which belong to From c24763b3da819c1b9ddcc6cf99897cde038ae63d Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Wed, 5 Jan 2022 11:04:22 +0100 Subject: [PATCH 07/17] lnd+lnwallet: add logger for btcwallet+rpcwallet --- lnwallet/btcwallet/log.go | 32 ++++++++++++++++++++++++++++++++ lnwallet/log.go | 1 - lnwallet/rpcwallet/log.go | 32 ++++++++++++++++++++++++++++++++ log.go | 4 ++++ 4 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 lnwallet/btcwallet/log.go create mode 100644 lnwallet/rpcwallet/log.go diff --git a/lnwallet/btcwallet/log.go b/lnwallet/btcwallet/log.go new file mode 100644 index 000000000..878db08e6 --- /dev/null +++ b/lnwallet/btcwallet/log.go @@ -0,0 +1,32 @@ +package btcwallet + +import ( + "github.com/btcsuite/btclog" + "github.com/lightningnetwork/lnd/build" +) + +// Subsystem defines the logging code for this subsystem. +const Subsystem = "BTWL" + +// log is a logger that is initialized with no output filters. This means the +// package will not perform any logging by default until the caller requests +// it. +var log btclog.Logger + +// The default amount of logging is none. +func init() { + UseLogger(build.NewSubLogger(Subsystem, nil)) +} + +// DisableLog disables all library log output. Logging output is disabled by +// default until UseLogger is called. +func DisableLog() { + UseLogger(btclog.Disabled) +} + +// UseLogger uses a specified Logger to output package logging info. This +// should be used in preference to SetLogWriter if the caller is also using +// btclog. +func UseLogger(logger btclog.Logger) { + log = logger +} diff --git a/lnwallet/log.go b/lnwallet/log.go index 31881e2d1..341edeca7 100644 --- a/lnwallet/log.go +++ b/lnwallet/log.go @@ -5,7 +5,6 @@ import ( "github.com/btcsuite/btcwallet/chain" btcwallet "github.com/btcsuite/btcwallet/wallet" "github.com/btcsuite/btcwallet/wtxmgr" - "github.com/lightningnetwork/lnd/build" "github.com/lightningnetwork/lnd/lnwallet/chainfee" ) diff --git a/lnwallet/rpcwallet/log.go b/lnwallet/rpcwallet/log.go new file mode 100644 index 000000000..190aa623a --- /dev/null +++ b/lnwallet/rpcwallet/log.go @@ -0,0 +1,32 @@ +package rpcwallet + +import ( + "github.com/btcsuite/btclog" + "github.com/lightningnetwork/lnd/build" +) + +// Subsystem defines the logging code for this subsystem. +const Subsystem = "RPWL" + +// log is a logger that is initialized with no output filters. This means the +// package will not perform any logging by default until the caller requests +// it. +var log btclog.Logger + +// The default amount of logging is none. +func init() { + UseLogger(build.NewSubLogger(Subsystem, nil)) +} + +// DisableLog disables all library log output. Logging output is disabled by +// default until UseLogger is called. +func DisableLog() { + UseLogger(btclog.Disabled) +} + +// UseLogger uses a specified Logger to output package logging info. This +// should be used in preference to SetLogWriter if the caller is also using +// btclog. +func UseLogger(logger btclog.Logger) { + log = logger +} diff --git a/log.go b/log.go index 68b592bb4..be289b6c0 100644 --- a/log.go +++ b/log.go @@ -29,8 +29,10 @@ import ( "github.com/lightningnetwork/lnd/lnrpc/verrpc" "github.com/lightningnetwork/lnd/lnrpc/walletrpc" "github.com/lightningnetwork/lnd/lnwallet" + "github.com/lightningnetwork/lnd/lnwallet/btcwallet" "github.com/lightningnetwork/lnd/lnwallet/chancloser" "github.com/lightningnetwork/lnd/lnwallet/chanfunding" + "github.com/lightningnetwork/lnd/lnwallet/rpcwallet" "github.com/lightningnetwork/lnd/monitoring" "github.com/lightningnetwork/lnd/netann" "github.com/lightningnetwork/lnd/peer" @@ -163,6 +165,8 @@ func SetupLoggers(root *build.RotatingLogWriter, interceptor signal.Interceptor) AddSubLogger(root, cluster.Subsystem, interceptor, cluster.UseLogger) AddSubLogger(root, rpcperms.Subsystem, interceptor, rpcperms.UseLogger) AddSubLogger(root, tor.Subsystem, interceptor, tor.UseLogger) + AddSubLogger(root, btcwallet.Subsystem, interceptor, btcwallet.UseLogger) + AddSubLogger(root, rpcwallet.Subsystem, interceptor, rpcwallet.UseLogger) } // AddSubLogger is a helper method to conveniently create and register the From d135b638f69c6ceb9bdfa52f81c3784e1d340db5 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Wed, 5 Jan 2022 11:04:23 +0100 Subject: [PATCH 08/17] btcwallet: add SignPsbt --- lntest/mock/walletcontroller.go | 5 + lnwallet/btcwallet/psbt.go | 191 ++++++++++++++++++ lnwallet/btcwallet/psbt_test.go | 337 ++++++++++++++++++++++++++++++++ lnwallet/interface.go | 13 ++ 4 files changed, 546 insertions(+) create mode 100644 lnwallet/btcwallet/psbt_test.go diff --git a/lntest/mock/walletcontroller.go b/lntest/mock/walletcontroller.go index 72da14b14..f524695a5 100644 --- a/lntest/mock/walletcontroller.go +++ b/lntest/mock/walletcontroller.go @@ -189,6 +189,11 @@ func (w *WalletController) FundPsbt(*psbt.Packet, int32, chainfee.SatPerKWeight, return 0, nil } +// SignPsbt currently does nothing. +func (w *WalletController) SignPsbt(*psbt.Packet) error { + return nil +} + // FinalizePsbt currently does nothing. func (w *WalletController) FinalizePsbt(_ *psbt.Packet, _ string) error { return nil diff --git a/lnwallet/btcwallet/psbt.go b/lnwallet/btcwallet/psbt.go index 8e657f0c5..45cbcd6a0 100644 --- a/lnwallet/btcwallet/psbt.go +++ b/lnwallet/btcwallet/psbt.go @@ -1,13 +1,33 @@ package btcwallet import ( + "bytes" + "fmt" + + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil/psbt" "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet/chainfee" ) +var ( + // PsbtKeyTypeInputSignatureTweakSingle is a custom/proprietary PSBT key + // for an input that specifies what single tweak should be applied to + // the key before signing the input. The value 51 is leet speak for + // "si", short for "single". + PsbtKeyTypeInputSignatureTweakSingle = []byte{0x51} + + // PsbtKeyTypeInputSignatureTweakDouble is a custom/proprietary PSBT key + // for an input that specifies what double tweak should be applied to + // the key before signing the input. The value d0 is leet speak for + // "do", short for "double". + PsbtKeyTypeInputSignatureTweakDouble = []byte{0xd0} +) + // FundPsbt creates a fully populated PSBT packet that contains enough inputs to // fund the outputs specified in the passed in packet with the specified fee // rate. If there is change left, a change output from the internal wallet is @@ -64,6 +84,177 @@ func (b *BtcWallet) FundPsbt(packet *psbt.Packet, minConfs int32, ) } +// SignPsbt expects a partial transaction with all inputs and outputs fully +// declared and tries to sign all unsigned inputs that have all required fields +// (UTXO information, BIP32 derivation information, witness or sig scripts) set. +// If no error is returned, the PSBT is ready to be given to the next signer or +// to be finalized if lnd was the last signer. +// +// NOTE: This method only signs inputs (and only those it can sign), it does not +// perform any other tasks (such as coin selection, UTXO locking or +// input/output/fee value validation, PSBT finalization). Any input that is +// incomplete will be skipped. +func (b *BtcWallet) SignPsbt(packet *psbt.Packet) error { + // Let's check that this is actually something we can and want to sign. + // We need at least one input and one output. + err := psbt.VerifyInputOutputLen(packet, true, true) + if err != nil { + return err + } + + // Go through each input that doesn't have final witness data attached + // to it already and try to sign it. If there is nothing more to sign or + // there are inputs that we don't know how to sign, we won't return any + // error. So it's possible we're not the final signer. + tx := packet.UnsignedTx + sigHashes := txscript.NewTxSigHashes(tx) + for idx := range tx.TxIn { + in := packet.Inputs[idx] + + // We can only sign if we have UTXO information available. Since + // we don't finalize, we just skip over any input that we know + // we can't do anything with. Since we only support signing + // witness inputs, we only look at the witness UTXO being set. + if in.WitnessUtxo == nil { + continue + } + + // Skip this input if it's got final witness data attached. + if len(in.FinalScriptWitness) > 0 { + continue + } + + // Skip this input if there is no BIP32 derivation info + // available. + if len(in.Bip32Derivation) == 0 { + continue + } + + // TODO(guggero): For multisig, we'll need to find out what key + // to use and there should be multiple derivation paths in the + // BIP32 derivation field. + + // Let's try and derive the key now. This method will decide if + // it's a BIP49/84 key for normal on-chain funds or a key of the + // custom purpose 1017 key scope. + derivationInfo := in.Bip32Derivation[0] + privKey, err := b.deriveKeyByBIP32Path(derivationInfo.Bip32Path) + if err != nil { + log.Warnf("SignPsbt: Skipping input %d, error "+ + "deriving signing key: %v", idx, err) + continue + } + + // We need to make sure we actually derived the key that was + // expected to be derived. + pubKeysEqual := bytes.Equal( + derivationInfo.PubKey, + privKey.PubKey().SerializeCompressed(), + ) + if !pubKeysEqual { + log.Warnf("SignPsbt: Skipping input %d, derived "+ + "public key %x does not match bip32 "+ + "derivation info public key %x", idx, + privKey.PubKey().SerializeCompressed(), + derivationInfo.PubKey) + continue + } + + // Do we need to tweak anything? Single or double tweaks are + // sent as custom/proprietary fields in the PSBT input section. + privKey = maybeTweakPrivKeyPsbt(in.Unknowns, privKey) + pubKeyBytes := privKey.PubKey().SerializeCompressed() + + // Extract the correct witness and/or legacy scripts now, + // depending on the type of input we sign. The txscript package + // has the peculiar requirement that the PkScript of a P2PKH + // must be given as the witness script in order for it to arrive + // at the correct sighash. That's why we call it subScript here + // instead of witness script. + subScript, scriptSig, err := prepareScripts(in) + if err != nil { + // We derived the correct key so we _are_ expected to + // sign this. Not being able to sign at this point means + // there's something wrong. + return fmt.Errorf("error deriving script for input "+ + "%d: %v", idx, err) + } + + // We have everything we need for signing the input now. + sig, err := txscript.RawTxInWitnessSignature( + tx, sigHashes, idx, in.WitnessUtxo.Value, subScript, + in.SighashType, privKey, + ) + if err != nil { + return fmt.Errorf("error signing input %d: %v", idx, + err) + } + packet.Inputs[idx].FinalScriptSig = scriptSig + packet.Inputs[idx].PartialSigs = append( + packet.Inputs[idx].PartialSigs, &psbt.PartialSig{ + PubKey: pubKeyBytes, + Signature: sig, + }, + ) + } + + return nil +} + +// prepareScripts returns the appropriate witness and/or legacy scripts, +// depending on the type of input that should be signed. +func prepareScripts(in psbt.PInput) ([]byte, []byte, error) { + switch { + // It's a NP2WKH input: + case len(in.RedeemScript) > 0: + builder := txscript.NewScriptBuilder() + builder.AddData(in.RedeemScript) + sigScript, err := builder.Script() + if err != nil { + return nil, nil, fmt.Errorf("error building np2wkh "+ + "script: %v", err) + } + + return in.RedeemScript, sigScript, nil + + // It's a P2WSH input: + case len(in.WitnessScript) > 0: + return in.WitnessScript, nil, nil + + // It's a P2WKH input: + default: + return in.WitnessUtxo.PkScript, nil, nil + } +} + +// maybeTweakPrivKeyPsbt examines if there are any tweak parameters given in the +// custom/proprietary PSBT fields and may perform a mapping on the passed +// private key in order to utilize the tweaks, if populated. +func maybeTweakPrivKeyPsbt(unknowns []*psbt.Unknown, + privKey *btcec.PrivateKey) *btcec.PrivateKey { + + // There can be other custom/unknown keys in a PSBT that we just ignore. + // Key tweaking is optional and only one tweak (single _or_ double) can + // ever be applied (at least for any use cases described in the BOLT + // spec). + for _, u := range unknowns { + if bytes.Equal(u.Key, PsbtKeyTypeInputSignatureTweakSingle) { + return input.TweakPrivKey(privKey, u.Value) + } + + if bytes.Equal(u.Key, PsbtKeyTypeInputSignatureTweakDouble) { + doubleTweakKey, _ := btcec.PrivKeyFromBytes( + btcec.S256(), u.Value, + ) + return input.DeriveRevocationPrivKey( + privKey, doubleTweakKey, + ) + } + } + + return privKey +} + // FinalizePsbt expects a partial transaction with all inputs and outputs fully // declared and tries to sign all inputs that belong to the specified account. // Lnd must be the last signer of the transaction. That means, if there are any diff --git a/lnwallet/btcwallet/psbt_test.go b/lnwallet/btcwallet/psbt_test.go new file mode 100644 index 000000000..5210d7a7c --- /dev/null +++ b/lnwallet/btcwallet/psbt_test.go @@ -0,0 +1,337 @@ +package btcwallet + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "fmt" + "testing" + + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcutil/psbt" + "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/keychain" + "github.com/stretchr/testify/require" +) + +var ( + netParams = &chaincfg.RegressionNetParams + testValue int64 = 345678 + testCSVTimeout uint32 = 2016 + + testCommitSecretBytes, _ = hex.DecodeString( + "9f1f0db609718cf70c580aec6a0e570c3f086ec85a2a6119295b1d64240d" + + "aca5", + ) + testCommitSecret, testCommitPoint = btcec.PrivKeyFromBytes( + btcec.S256(), testCommitSecretBytes, + ) + + remoteRevocationBasePubKeyBytes, _ = hex.DecodeString( + "02baf067bfd1a6cf7229c7c459b106d384ad33e948ea1d561f2034475ff1" + + "7359fb", + ) + remoteRevocationBasePubKey, _ = btcec.ParsePubKey( + remoteRevocationBasePubKeyBytes, btcec.S256(), + ) + + testTweakSingle, _ = hex.DecodeString( + "020143a30cf6b71ca2af01efbd1758a04b4c7f5c2299f2ea63a8a6b58107" + + "63b1ed", + ) +) + +// testInputType is a type that represents different types of inputs that are +// signed within a PSBT. +type testInputType uint8 + +const ( + plainP2WKH testInputType = 0 + tweakedP2WKH testInputType = 1 + nestedP2WKH testInputType = 2 + singleKeyP2WSH testInputType = 3 + singleKeyDoubleTweakedP2WSH testInputType = 4 +) + +func (i testInputType) keyPath() []uint32 { + switch i { + case nestedP2WKH: + return []uint32{ + hardenedKey(waddrmgr.KeyScopeBIP0049Plus.Purpose), + hardenedKey(0), + hardenedKey(0), + 0, 0, + } + + case singleKeyP2WSH: + return []uint32{ + hardenedKey(keychain.BIP0043Purpose), + hardenedKey(netParams.HDCoinType), + hardenedKey(uint32(keychain.KeyFamilyPaymentBase)), + 0, 7, + } + + case singleKeyDoubleTweakedP2WSH: + return []uint32{ + hardenedKey(keychain.BIP0043Purpose), + hardenedKey(netParams.HDCoinType), + hardenedKey(uint32(keychain.KeyFamilyDelayBase)), + 0, 9, + } + + default: + return []uint32{ + hardenedKey(waddrmgr.KeyScopeBIP0084.Purpose), + hardenedKey(0), + hardenedKey(0), + 0, 0, + } + } +} + +func (i testInputType) output(t *testing.T, + privKey *btcec.PrivateKey) (*wire.TxOut, []byte) { + + var ( + addr btcutil.Address + witnessScript []byte + err error + ) + switch i { + case plainP2WKH: + h := btcutil.Hash160(privKey.PubKey().SerializeCompressed()) + addr, err = btcutil.NewAddressWitnessPubKeyHash(h, netParams) + require.NoError(t, err) + + case tweakedP2WKH: + privKey = input.TweakPrivKey(privKey, testTweakSingle) + + h := btcutil.Hash160(privKey.PubKey().SerializeCompressed()) + addr, err = btcutil.NewAddressWitnessPubKeyHash(h, netParams) + require.NoError(t, err) + + case nestedP2WKH: + h := btcutil.Hash160(privKey.PubKey().SerializeCompressed()) + witnessAddr, err := btcutil.NewAddressWitnessPubKeyHash( + h, netParams, + ) + require.NoError(t, err) + + witnessProgram, err := txscript.PayToAddrScript(witnessAddr) + require.NoError(t, err) + + addr, err = btcutil.NewAddressScriptHash( + witnessProgram, netParams, + ) + require.NoError(t, err) + + case singleKeyP2WSH: + // We're simulating a delay-to-self script which we're going to + // spend through the time lock path. We don't actually need to + // know the private key of the remote revocation base key. + revokeKey := input.DeriveRevocationPubkey( + remoteRevocationBasePubKey, testCommitPoint, + ) + witnessScript, err = input.CommitScriptToSelf( + testCSVTimeout, privKey.PubKey(), revokeKey, + ) + require.NoError(t, err) + + h := sha256.Sum256(witnessScript) + addr, err = btcutil.NewAddressWitnessScriptHash(h[:], netParams) + require.NoError(t, err) + + case singleKeyDoubleTweakedP2WSH: + // We're simulating breaching a remote party's delay-to-self + // output which we're going to spend through the revocation + // path. In that case the self key is the other party's self key + // and, we only know the revocation base private key and commit + // secret. + revokeKey := input.DeriveRevocationPubkey( + privKey.PubKey(), testCommitPoint, + ) + witnessScript, err = input.CommitScriptToSelf( + testCSVTimeout, remoteRevocationBasePubKey, revokeKey, + ) + require.NoError(t, err) + + h := sha256.Sum256(witnessScript) + addr, err = btcutil.NewAddressWitnessScriptHash(h[:], netParams) + require.NoError(t, err) + + default: + t.Fatalf("invalid input type") + } + + pkScript, err := txscript.PayToAddrScript(addr) + require.NoError(t, err) + return &wire.TxOut{ + Value: testValue, + PkScript: pkScript, + }, witnessScript +} + +func (i testInputType) decorateInput(t *testing.T, privKey *btcec.PrivateKey, + in *psbt.PInput) { + + switch i { + case tweakedP2WKH: + in.Unknowns = []*psbt.Unknown{{ + Key: PsbtKeyTypeInputSignatureTweakSingle, + Value: testTweakSingle, + }} + + case nestedP2WKH: + h := btcutil.Hash160(privKey.PubKey().SerializeCompressed()) + witnessAddr, err := btcutil.NewAddressWitnessPubKeyHash( + h, netParams, + ) + require.NoError(t, err) + + witnessProgram, err := txscript.PayToAddrScript(witnessAddr) + require.NoError(t, err) + in.RedeemScript = witnessProgram + + case singleKeyDoubleTweakedP2WSH: + in.Unknowns = []*psbt.Unknown{{ + Key: PsbtKeyTypeInputSignatureTweakDouble, + Value: testCommitSecret.Serialize(), + }} + } +} + +func (i testInputType) beforeFinalize(t *testing.T, packet *psbt.Packet) { + in := &packet.Inputs[0] + sigBytes := in.PartialSigs[0].Signature + pubKeyBytes := in.PartialSigs[0].PubKey + + var witnessStack wire.TxWitness + switch i { + case singleKeyP2WSH: + witnessStack = make([][]byte, 3) + witnessStack[0] = sigBytes + witnessStack[1] = nil + witnessStack[2] = in.WitnessScript + + case singleKeyDoubleTweakedP2WSH: + // Place a 1 as the first item in the evaluated witness stack to + // force script execution to the revocation clause. + witnessStack = make([][]byte, 3) + witnessStack[0] = sigBytes + witnessStack[1] = []byte{1} + witnessStack[2] = in.WitnessScript + + default: + witnessStack = make([][]byte, 2) + witnessStack[0] = sigBytes + witnessStack[1] = pubKeyBytes + } + + var err error + in.FinalScriptWitness, err = serializeTxWitness(witnessStack) + require.NoError(t, err) +} + +// serializeTxWitness return the wire witness stack into raw bytes. +func serializeTxWitness(txWitness wire.TxWitness) ([]byte, error) { + var witnessBytes bytes.Buffer + err := psbt.WriteTxWitness(&witnessBytes, txWitness) + if err != nil { + return nil, fmt.Errorf("error serializing witness: %v", err) + } + + return witnessBytes.Bytes(), nil +} + +// TestSignPsbt tests the PSBT signing functionality. +func TestSignPsbt(t *testing.T) { + w, cleanup := newTestWallet(t, netParams, seedBytes) + defer cleanup() + + testCases := []struct { + name string + inputType testInputType + }{{ + name: "plain P2WKH", + inputType: plainP2WKH, + }, { + name: "tweaked P2WKH", + inputType: tweakedP2WKH, + }, { + name: "nested P2WKH", + inputType: nestedP2WKH, + }, { + name: "single key P2WSH", + inputType: singleKeyP2WSH, + }, { + name: "single key double tweaked P2WSH", + inputType: singleKeyDoubleTweakedP2WSH, + }} + + for _, tc := range testCases { + tc := tc + + // This is the private key we're going to sign with. + privKey, err := w.deriveKeyByBIP32Path(tc.inputType.keyPath()) + require.NoError(t, err) + + txOut, witnessScript := tc.inputType.output(t, privKey) + + // Create the reference transaction that has the input that is + // going to be spent by our PSBT. + refTx := wire.NewMsgTx(2) + refTx.AddTxIn(&wire.TxIn{}) + refTx.AddTxOut(txOut) + + // Create the unsigned spend transaction that is going to be the + // main content of our PSBT. + spendTx := wire.NewMsgTx(2) + spendTx.LockTime = testCSVTimeout + spendTx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: wire.OutPoint{ + Hash: refTx.TxHash(), + Index: 0, + }, + Sequence: testCSVTimeout, + }) + spendTx.AddTxOut(txOut) + + // Convert it to a PSBT now and add all required signing + // metadata to it. + packet, err := psbt.NewFromUnsignedTx(spendTx) + require.NoError(t, err) + packet.Inputs[0].WitnessScript = witnessScript + packet.Inputs[0].SighashType = txscript.SigHashAll + packet.Inputs[0].WitnessUtxo = refTx.TxOut[0] + packet.Inputs[0].Bip32Derivation = []*psbt.Bip32Derivation{{ + PubKey: privKey.PubKey().SerializeCompressed(), + Bip32Path: tc.inputType.keyPath(), + }} + tc.inputType.decorateInput(t, privKey, &packet.Inputs[0]) + + // Let the wallet do its job. We expect to be the only signer + // for this PSBT, so we'll be able to finalize it later. + err = w.SignPsbt(packet) + require.NoError(t, err) + + // If the witness stack needs to be assembled, give the caller + // the option to do that now. + tc.inputType.beforeFinalize(t, packet) + + finalTx, err := psbt.Extract(packet) + require.NoError(t, err) + + vm, err := txscript.NewEngine( + refTx.TxOut[0].PkScript, finalTx, 0, + txscript.StandardVerifyFlags, nil, nil, + refTx.TxOut[0].Value, + ) + require.NoError(t, err) + require.NoError(t, vm.Execute()) + } +} diff --git a/lnwallet/interface.go b/lnwallet/interface.go index 455d48b00..70b7b0331 100644 --- a/lnwallet/interface.go +++ b/lnwallet/interface.go @@ -364,6 +364,19 @@ type WalletController interface { FundPsbt(packet *psbt.Packet, minConfs int32, feeRate chainfee.SatPerKWeight, account string) (int32, error) + // SignPsbt expects a partial transaction with all inputs and outputs + // fully declared and tries to sign all unsigned inputs that have all + // required fields (UTXO information, BIP32 derivation information, + // witness or sig scripts) set. + // If no error is returned, the PSBT is ready to be given to the next + // signer or to be finalized if lnd was the last signer. + // + // NOTE: This method only signs inputs (and only those it can sign), it + // does not perform any other tasks (such as coin selection, UTXO + // locking or input/output/fee value validation, PSBT finalization). Any + // input that is incomplete will be skipped. + SignPsbt(packet *psbt.Packet) error + // FinalizePsbt expects a partial transaction with all inputs and // outputs fully declared and tries to sign all inputs that belong to // the specified account. Lnd must be the last signer of the From 8994d86a1da72261f19c877b47d320b34897df2f Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Wed, 5 Jan 2022 11:04:24 +0100 Subject: [PATCH 09/17] walletrpc+itest: add SignPsbt RPC --- lnrpc/walletrpc/walletkit.pb.go | 559 +++++++++++++++---------- lnrpc/walletrpc/walletkit.pb.gw.go | 81 ++++ lnrpc/walletrpc/walletkit.pb.json.go | 25 ++ lnrpc/walletrpc/walletkit.proto | 28 ++ lnrpc/walletrpc/walletkit.swagger.json | 54 +++ lnrpc/walletrpc/walletkit.yaml | 3 + lnrpc/walletrpc/walletkit_grpc.pb.go | 60 +++ lnrpc/walletrpc/walletkit_server.go | 45 ++ lntest/itest/lnd_psbt_test.go | 233 +++++++++-- lntest/itest/lnd_remote_signer_test.go | 1 + lntest/itest/lnd_test_list_on_test.go | 4 + 11 files changed, 844 insertions(+), 249 deletions(-) diff --git a/lnrpc/walletrpc/walletkit.pb.go b/lnrpc/walletrpc/walletkit.pb.go index 0f17d62e2..4a1686da2 100644 --- a/lnrpc/walletrpc/walletkit.pb.go +++ b/lnrpc/walletrpc/walletkit.pb.go @@ -2542,6 +2542,104 @@ func (x *UtxoLease) GetExpiration() uint64 { return 0 } +type SignPsbtRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // + //The PSBT that should be signed. The PSBT must contain all required inputs, + //outputs, UTXO data and custom fields required to identify the signing key. + FundedPsbt []byte `protobuf:"bytes,1,opt,name=funded_psbt,json=fundedPsbt,proto3" json:"funded_psbt,omitempty"` +} + +func (x *SignPsbtRequest) Reset() { + *x = SignPsbtRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_walletrpc_walletkit_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SignPsbtRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SignPsbtRequest) ProtoMessage() {} + +func (x *SignPsbtRequest) ProtoReflect() protoreflect.Message { + mi := &file_walletrpc_walletkit_proto_msgTypes[35] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SignPsbtRequest.ProtoReflect.Descriptor instead. +func (*SignPsbtRequest) Descriptor() ([]byte, []int) { + return file_walletrpc_walletkit_proto_rawDescGZIP(), []int{35} +} + +func (x *SignPsbtRequest) GetFundedPsbt() []byte { + if x != nil { + return x.FundedPsbt + } + return nil +} + +type SignPsbtResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The signed transaction in PSBT format. + SignedPsbt []byte `protobuf:"bytes,1,opt,name=signed_psbt,json=signedPsbt,proto3" json:"signed_psbt,omitempty"` +} + +func (x *SignPsbtResponse) Reset() { + *x = SignPsbtResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_walletrpc_walletkit_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SignPsbtResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SignPsbtResponse) ProtoMessage() {} + +func (x *SignPsbtResponse) ProtoReflect() protoreflect.Message { + mi := &file_walletrpc_walletkit_proto_msgTypes[36] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SignPsbtResponse.ProtoReflect.Descriptor instead. +func (*SignPsbtResponse) Descriptor() ([]byte, []int) { + return file_walletrpc_walletkit_proto_rawDescGZIP(), []int{36} +} + +func (x *SignPsbtResponse) GetSignedPsbt() []byte { + if x != nil { + return x.SignedPsbt + } + return nil +} + type FinalizePsbtRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -2561,7 +2659,7 @@ type FinalizePsbtRequest struct { func (x *FinalizePsbtRequest) Reset() { *x = FinalizePsbtRequest{} if protoimpl.UnsafeEnabled { - mi := &file_walletrpc_walletkit_proto_msgTypes[35] + mi := &file_walletrpc_walletkit_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2574,7 +2672,7 @@ func (x *FinalizePsbtRequest) String() string { func (*FinalizePsbtRequest) ProtoMessage() {} func (x *FinalizePsbtRequest) ProtoReflect() protoreflect.Message { - mi := &file_walletrpc_walletkit_proto_msgTypes[35] + mi := &file_walletrpc_walletkit_proto_msgTypes[37] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2587,7 +2685,7 @@ func (x *FinalizePsbtRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FinalizePsbtRequest.ProtoReflect.Descriptor instead. func (*FinalizePsbtRequest) Descriptor() ([]byte, []int) { - return file_walletrpc_walletkit_proto_rawDescGZIP(), []int{35} + return file_walletrpc_walletkit_proto_rawDescGZIP(), []int{37} } func (x *FinalizePsbtRequest) GetFundedPsbt() []byte { @@ -2618,7 +2716,7 @@ type FinalizePsbtResponse struct { func (x *FinalizePsbtResponse) Reset() { *x = FinalizePsbtResponse{} if protoimpl.UnsafeEnabled { - mi := &file_walletrpc_walletkit_proto_msgTypes[36] + mi := &file_walletrpc_walletkit_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2631,7 +2729,7 @@ func (x *FinalizePsbtResponse) String() string { func (*FinalizePsbtResponse) ProtoMessage() {} func (x *FinalizePsbtResponse) ProtoReflect() protoreflect.Message { - mi := &file_walletrpc_walletkit_proto_msgTypes[36] + mi := &file_walletrpc_walletkit_proto_msgTypes[38] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2644,7 +2742,7 @@ func (x *FinalizePsbtResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use FinalizePsbtResponse.ProtoReflect.Descriptor instead. func (*FinalizePsbtResponse) Descriptor() ([]byte, []int) { - return file_walletrpc_walletkit_proto_rawDescGZIP(), []int{36} + return file_walletrpc_walletkit_proto_rawDescGZIP(), []int{38} } func (x *FinalizePsbtResponse) GetSignedPsbt() []byte { @@ -2670,7 +2768,7 @@ type ListLeasesRequest struct { func (x *ListLeasesRequest) Reset() { *x = ListLeasesRequest{} if protoimpl.UnsafeEnabled { - mi := &file_walletrpc_walletkit_proto_msgTypes[37] + mi := &file_walletrpc_walletkit_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2683,7 +2781,7 @@ func (x *ListLeasesRequest) String() string { func (*ListLeasesRequest) ProtoMessage() {} func (x *ListLeasesRequest) ProtoReflect() protoreflect.Message { - mi := &file_walletrpc_walletkit_proto_msgTypes[37] + mi := &file_walletrpc_walletkit_proto_msgTypes[39] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2696,7 +2794,7 @@ func (x *ListLeasesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListLeasesRequest.ProtoReflect.Descriptor instead. func (*ListLeasesRequest) Descriptor() ([]byte, []int) { - return file_walletrpc_walletkit_proto_rawDescGZIP(), []int{37} + return file_walletrpc_walletkit_proto_rawDescGZIP(), []int{39} } type ListLeasesResponse struct { @@ -2711,7 +2809,7 @@ type ListLeasesResponse struct { func (x *ListLeasesResponse) Reset() { *x = ListLeasesResponse{} if protoimpl.UnsafeEnabled { - mi := &file_walletrpc_walletkit_proto_msgTypes[38] + mi := &file_walletrpc_walletkit_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2724,7 +2822,7 @@ func (x *ListLeasesResponse) String() string { func (*ListLeasesResponse) ProtoMessage() {} func (x *ListLeasesResponse) ProtoReflect() protoreflect.Message { - mi := &file_walletrpc_walletkit_proto_msgTypes[38] + mi := &file_walletrpc_walletkit_proto_msgTypes[40] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2737,7 +2835,7 @@ func (x *ListLeasesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListLeasesResponse.ProtoReflect.Descriptor instead. func (*ListLeasesResponse) Descriptor() ([]byte, []int) { - return file_walletrpc_walletkit_proto_rawDescGZIP(), []int{38} + return file_walletrpc_walletkit_proto_rawDescGZIP(), []int{40} } func (x *ListLeasesResponse) GetLockedUtxos() []*UtxoLease { @@ -2762,7 +2860,7 @@ type ListSweepsResponse_TransactionIDs struct { func (x *ListSweepsResponse_TransactionIDs) Reset() { *x = ListSweepsResponse_TransactionIDs{} if protoimpl.UnsafeEnabled { - mi := &file_walletrpc_walletkit_proto_msgTypes[39] + mi := &file_walletrpc_walletkit_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2775,7 +2873,7 @@ func (x *ListSweepsResponse_TransactionIDs) String() string { func (*ListSweepsResponse_TransactionIDs) ProtoMessage() {} func (x *ListSweepsResponse_TransactionIDs) ProtoReflect() protoreflect.Message { - mi := &file_walletrpc_walletkit_proto_msgTypes[39] + mi := &file_walletrpc_walletkit_proto_msgTypes[41] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3069,153 +3167,164 @@ var file_walletrpc_walletkit_proto_rawDesc = []byte{ 0x32, 0x0f, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4f, 0x75, 0x74, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x08, 0x6f, 0x75, 0x74, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, - 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x50, 0x0a, 0x13, 0x46, - 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x50, 0x73, 0x62, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x66, 0x75, 0x6e, 0x64, 0x65, 0x64, 0x5f, 0x70, 0x73, 0x62, - 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x66, 0x75, 0x6e, 0x64, 0x65, 0x64, 0x50, - 0x73, 0x62, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x05, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x59, 0x0a, - 0x14, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x50, 0x73, 0x62, 0x74, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, - 0x70, 0x73, 0x62, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x73, 0x69, 0x67, 0x6e, - 0x65, 0x64, 0x50, 0x73, 0x62, 0x74, 0x12, 0x20, 0x0a, 0x0c, 0x72, 0x61, 0x77, 0x5f, 0x66, 0x69, - 0x6e, 0x61, 0x6c, 0x5f, 0x74, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x72, 0x61, - 0x77, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x54, 0x78, 0x22, 0x13, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, - 0x4c, 0x65, 0x61, 0x73, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x4d, 0x0a, - 0x12, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x65, 0x61, 0x73, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x0c, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x5f, 0x75, 0x74, - 0x78, 0x6f, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x77, 0x61, 0x6c, 0x6c, - 0x65, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x55, 0x74, 0x78, 0x6f, 0x4c, 0x65, 0x61, 0x73, 0x65, 0x52, - 0x0b, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x55, 0x74, 0x78, 0x6f, 0x73, 0x2a, 0x7a, 0x0a, 0x0b, - 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, - 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x17, 0x0a, 0x13, 0x57, 0x49, 0x54, 0x4e, + 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x32, 0x0a, 0x0f, 0x53, + 0x69, 0x67, 0x6e, 0x50, 0x73, 0x62, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1f, + 0x0a, 0x0b, 0x66, 0x75, 0x6e, 0x64, 0x65, 0x64, 0x5f, 0x70, 0x73, 0x62, 0x74, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x66, 0x75, 0x6e, 0x64, 0x65, 0x64, 0x50, 0x73, 0x62, 0x74, 0x22, + 0x33, 0x0a, 0x10, 0x53, 0x69, 0x67, 0x6e, 0x50, 0x73, 0x62, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x70, 0x73, + 0x62, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, + 0x50, 0x73, 0x62, 0x74, 0x22, 0x50, 0x0a, 0x13, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, + 0x50, 0x73, 0x62, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x66, + 0x75, 0x6e, 0x64, 0x65, 0x64, 0x5f, 0x70, 0x73, 0x62, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x0a, 0x66, 0x75, 0x6e, 0x64, 0x65, 0x64, 0x50, 0x73, 0x62, 0x74, 0x12, 0x18, 0x0a, 0x07, + 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, + 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x59, 0x0a, 0x14, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, + 0x7a, 0x65, 0x50, 0x73, 0x62, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1f, + 0x0a, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x70, 0x73, 0x62, 0x74, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x50, 0x73, 0x62, 0x74, 0x12, + 0x20, 0x0a, 0x0c, 0x72, 0x61, 0x77, 0x5f, 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x5f, 0x74, 0x78, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x72, 0x61, 0x77, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x54, + 0x78, 0x22, 0x13, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x65, 0x61, 0x73, 0x65, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x4d, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x65, + 0x61, 0x73, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x0c, + 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x5f, 0x75, 0x74, 0x78, 0x6f, 0x73, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x55, + 0x74, 0x78, 0x6f, 0x4c, 0x65, 0x61, 0x73, 0x65, 0x52, 0x0b, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, + 0x55, 0x74, 0x78, 0x6f, 0x73, 0x2a, 0x7a, 0x0a, 0x0b, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, + 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, + 0x00, 0x12, 0x17, 0x0a, 0x13, 0x57, 0x49, 0x54, 0x4e, 0x45, 0x53, 0x53, 0x5f, 0x50, 0x55, 0x42, + 0x4b, 0x45, 0x59, 0x5f, 0x48, 0x41, 0x53, 0x48, 0x10, 0x01, 0x12, 0x1e, 0x0a, 0x1a, 0x4e, 0x45, + 0x53, 0x54, 0x45, 0x44, 0x5f, 0x57, 0x49, 0x54, 0x4e, 0x45, 0x53, 0x53, 0x5f, 0x50, 0x55, 0x42, + 0x4b, 0x45, 0x59, 0x5f, 0x48, 0x41, 0x53, 0x48, 0x10, 0x02, 0x12, 0x25, 0x0a, 0x21, 0x48, 0x59, + 0x42, 0x52, 0x49, 0x44, 0x5f, 0x4e, 0x45, 0x53, 0x54, 0x45, 0x44, 0x5f, 0x57, 0x49, 0x54, 0x4e, 0x45, 0x53, 0x53, 0x5f, 0x50, 0x55, 0x42, 0x4b, 0x45, 0x59, 0x5f, 0x48, 0x41, 0x53, 0x48, 0x10, - 0x01, 0x12, 0x1e, 0x0a, 0x1a, 0x4e, 0x45, 0x53, 0x54, 0x45, 0x44, 0x5f, 0x57, 0x49, 0x54, 0x4e, - 0x45, 0x53, 0x53, 0x5f, 0x50, 0x55, 0x42, 0x4b, 0x45, 0x59, 0x5f, 0x48, 0x41, 0x53, 0x48, 0x10, - 0x02, 0x12, 0x25, 0x0a, 0x21, 0x48, 0x59, 0x42, 0x52, 0x49, 0x44, 0x5f, 0x4e, 0x45, 0x53, 0x54, - 0x45, 0x44, 0x5f, 0x57, 0x49, 0x54, 0x4e, 0x45, 0x53, 0x53, 0x5f, 0x50, 0x55, 0x42, 0x4b, 0x45, - 0x59, 0x5f, 0x48, 0x41, 0x53, 0x48, 0x10, 0x03, 0x2a, 0x99, 0x03, 0x0a, 0x0b, 0x57, 0x69, 0x74, - 0x6e, 0x65, 0x73, 0x73, 0x54, 0x79, 0x70, 0x65, 0x12, 0x13, 0x0a, 0x0f, 0x55, 0x4e, 0x4b, 0x4e, - 0x4f, 0x57, 0x4e, 0x5f, 0x57, 0x49, 0x54, 0x4e, 0x45, 0x53, 0x53, 0x10, 0x00, 0x12, 0x18, 0x0a, - 0x14, 0x43, 0x4f, 0x4d, 0x4d, 0x49, 0x54, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x54, 0x49, 0x4d, 0x45, - 0x5f, 0x4c, 0x4f, 0x43, 0x4b, 0x10, 0x01, 0x12, 0x17, 0x0a, 0x13, 0x43, 0x4f, 0x4d, 0x4d, 0x49, - 0x54, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x4e, 0x4f, 0x5f, 0x44, 0x45, 0x4c, 0x41, 0x59, 0x10, 0x02, - 0x12, 0x15, 0x0a, 0x11, 0x43, 0x4f, 0x4d, 0x4d, 0x49, 0x54, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x52, - 0x45, 0x56, 0x4f, 0x4b, 0x45, 0x10, 0x03, 0x12, 0x17, 0x0a, 0x13, 0x48, 0x54, 0x4c, 0x43, 0x5f, - 0x4f, 0x46, 0x46, 0x45, 0x52, 0x45, 0x44, 0x5f, 0x52, 0x45, 0x56, 0x4f, 0x4b, 0x45, 0x10, 0x04, - 0x12, 0x18, 0x0a, 0x14, 0x48, 0x54, 0x4c, 0x43, 0x5f, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x45, - 0x44, 0x5f, 0x52, 0x45, 0x56, 0x4f, 0x4b, 0x45, 0x10, 0x05, 0x12, 0x25, 0x0a, 0x21, 0x48, 0x54, - 0x4c, 0x43, 0x5f, 0x4f, 0x46, 0x46, 0x45, 0x52, 0x45, 0x44, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x4f, - 0x55, 0x54, 0x5f, 0x53, 0x45, 0x43, 0x4f, 0x4e, 0x44, 0x5f, 0x4c, 0x45, 0x56, 0x45, 0x4c, 0x10, - 0x06, 0x12, 0x26, 0x0a, 0x22, 0x48, 0x54, 0x4c, 0x43, 0x5f, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, - 0x45, 0x44, 0x5f, 0x53, 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, 0x5f, 0x53, 0x45, 0x43, 0x4f, 0x4e, - 0x44, 0x5f, 0x4c, 0x45, 0x56, 0x45, 0x4c, 0x10, 0x07, 0x12, 0x1f, 0x0a, 0x1b, 0x48, 0x54, 0x4c, - 0x43, 0x5f, 0x4f, 0x46, 0x46, 0x45, 0x52, 0x45, 0x44, 0x5f, 0x52, 0x45, 0x4d, 0x4f, 0x54, 0x45, - 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x4f, 0x55, 0x54, 0x10, 0x08, 0x12, 0x20, 0x0a, 0x1c, 0x48, 0x54, - 0x4c, 0x43, 0x5f, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x45, 0x44, 0x5f, 0x52, 0x45, 0x4d, 0x4f, - 0x54, 0x45, 0x5f, 0x53, 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0x09, 0x12, 0x1c, 0x0a, 0x18, - 0x48, 0x54, 0x4c, 0x43, 0x5f, 0x53, 0x45, 0x43, 0x4f, 0x4e, 0x44, 0x5f, 0x4c, 0x45, 0x56, 0x45, - 0x4c, 0x5f, 0x52, 0x45, 0x56, 0x4f, 0x4b, 0x45, 0x10, 0x0a, 0x12, 0x14, 0x0a, 0x10, 0x57, 0x49, - 0x54, 0x4e, 0x45, 0x53, 0x53, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x48, 0x41, 0x53, 0x48, 0x10, 0x0b, - 0x12, 0x1b, 0x0a, 0x17, 0x4e, 0x45, 0x53, 0x54, 0x45, 0x44, 0x5f, 0x57, 0x49, 0x54, 0x4e, 0x45, - 0x53, 0x53, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x48, 0x41, 0x53, 0x48, 0x10, 0x0c, 0x12, 0x15, 0x0a, - 0x11, 0x43, 0x4f, 0x4d, 0x4d, 0x49, 0x54, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x41, 0x4e, 0x43, 0x48, - 0x4f, 0x52, 0x10, 0x0d, 0x32, 0xb2, 0x0b, 0x0a, 0x09, 0x57, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x4b, - 0x69, 0x74, 0x12, 0x4c, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x6e, 0x73, 0x70, 0x65, 0x6e, - 0x74, 0x12, 0x1d, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, - 0x73, 0x74, 0x55, 0x6e, 0x73, 0x70, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x1e, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, - 0x74, 0x55, 0x6e, 0x73, 0x70, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x4c, 0x0a, 0x0b, 0x4c, 0x65, 0x61, 0x73, 0x65, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x12, - 0x1d, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x65, 0x61, 0x73, - 0x65, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, - 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x65, 0x61, 0x73, 0x65, - 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x52, - 0x0a, 0x0d, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x12, - 0x1f, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x6c, 0x65, - 0x61, 0x73, 0x65, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x20, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x6c, - 0x65, 0x61, 0x73, 0x65, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x49, 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x65, 0x61, 0x73, 0x65, 0x73, - 0x12, 0x1c, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, - 0x74, 0x4c, 0x65, 0x61, 0x73, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, - 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4c, - 0x65, 0x61, 0x73, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3a, 0x0a, - 0x0d, 0x44, 0x65, 0x72, 0x69, 0x76, 0x65, 0x4e, 0x65, 0x78, 0x74, 0x4b, 0x65, 0x79, 0x12, 0x11, - 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4b, 0x65, 0x79, 0x52, 0x65, - 0x71, 0x1a, 0x16, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4b, 0x65, 0x79, 0x44, - 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x12, 0x38, 0x0a, 0x09, 0x44, 0x65, 0x72, - 0x69, 0x76, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x13, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x72, 0x70, 0x63, - 0x2e, 0x4b, 0x65, 0x79, 0x4c, 0x6f, 0x63, 0x61, 0x74, 0x6f, 0x72, 0x1a, 0x16, 0x2e, 0x73, 0x69, - 0x67, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4b, 0x65, 0x79, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, - 0x74, 0x6f, 0x72, 0x12, 0x3b, 0x0a, 0x08, 0x4e, 0x65, 0x78, 0x74, 0x41, 0x64, 0x64, 0x72, 0x12, - 0x16, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x72, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, - 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x4f, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, - 0x12, 0x1e, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, - 0x74, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x1f, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, - 0x74, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x52, 0x0a, 0x0d, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x41, 0x63, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x12, 0x1f, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x49, - 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, 0x70, 0x63, 0x2e, - 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x58, 0x0a, 0x0f, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x50, - 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x21, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, - 0x74, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, - 0x63, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x77, 0x61, - 0x6c, 0x6c, 0x65, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x50, 0x75, - 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x48, 0x0a, 0x12, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, 0x70, - 0x63, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0x1a, 0x2e, - 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x73, - 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x0b, 0x53, 0x65, 0x6e, - 0x64, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x73, 0x12, 0x1d, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, - 0x74, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x73, + 0x03, 0x2a, 0x99, 0x03, 0x0a, 0x0b, 0x57, 0x69, 0x74, 0x6e, 0x65, 0x73, 0x73, 0x54, 0x79, 0x70, + 0x65, 0x12, 0x13, 0x0a, 0x0f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x5f, 0x57, 0x49, 0x54, + 0x4e, 0x45, 0x53, 0x53, 0x10, 0x00, 0x12, 0x18, 0x0a, 0x14, 0x43, 0x4f, 0x4d, 0x4d, 0x49, 0x54, + 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x5f, 0x4c, 0x4f, 0x43, 0x4b, 0x10, 0x01, + 0x12, 0x17, 0x0a, 0x13, 0x43, 0x4f, 0x4d, 0x4d, 0x49, 0x54, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x4e, + 0x4f, 0x5f, 0x44, 0x45, 0x4c, 0x41, 0x59, 0x10, 0x02, 0x12, 0x15, 0x0a, 0x11, 0x43, 0x4f, 0x4d, + 0x4d, 0x49, 0x54, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x52, 0x45, 0x56, 0x4f, 0x4b, 0x45, 0x10, 0x03, + 0x12, 0x17, 0x0a, 0x13, 0x48, 0x54, 0x4c, 0x43, 0x5f, 0x4f, 0x46, 0x46, 0x45, 0x52, 0x45, 0x44, + 0x5f, 0x52, 0x45, 0x56, 0x4f, 0x4b, 0x45, 0x10, 0x04, 0x12, 0x18, 0x0a, 0x14, 0x48, 0x54, 0x4c, + 0x43, 0x5f, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x45, 0x44, 0x5f, 0x52, 0x45, 0x56, 0x4f, 0x4b, + 0x45, 0x10, 0x05, 0x12, 0x25, 0x0a, 0x21, 0x48, 0x54, 0x4c, 0x43, 0x5f, 0x4f, 0x46, 0x46, 0x45, + 0x52, 0x45, 0x44, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x4f, 0x55, 0x54, 0x5f, 0x53, 0x45, 0x43, 0x4f, + 0x4e, 0x44, 0x5f, 0x4c, 0x45, 0x56, 0x45, 0x4c, 0x10, 0x06, 0x12, 0x26, 0x0a, 0x22, 0x48, 0x54, + 0x4c, 0x43, 0x5f, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x45, 0x44, 0x5f, 0x53, 0x55, 0x43, 0x43, + 0x45, 0x53, 0x53, 0x5f, 0x53, 0x45, 0x43, 0x4f, 0x4e, 0x44, 0x5f, 0x4c, 0x45, 0x56, 0x45, 0x4c, + 0x10, 0x07, 0x12, 0x1f, 0x0a, 0x1b, 0x48, 0x54, 0x4c, 0x43, 0x5f, 0x4f, 0x46, 0x46, 0x45, 0x52, + 0x45, 0x44, 0x5f, 0x52, 0x45, 0x4d, 0x4f, 0x54, 0x45, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x4f, 0x55, + 0x54, 0x10, 0x08, 0x12, 0x20, 0x0a, 0x1c, 0x48, 0x54, 0x4c, 0x43, 0x5f, 0x41, 0x43, 0x43, 0x45, + 0x50, 0x54, 0x45, 0x44, 0x5f, 0x52, 0x45, 0x4d, 0x4f, 0x54, 0x45, 0x5f, 0x53, 0x55, 0x43, 0x43, + 0x45, 0x53, 0x53, 0x10, 0x09, 0x12, 0x1c, 0x0a, 0x18, 0x48, 0x54, 0x4c, 0x43, 0x5f, 0x53, 0x45, + 0x43, 0x4f, 0x4e, 0x44, 0x5f, 0x4c, 0x45, 0x56, 0x45, 0x4c, 0x5f, 0x52, 0x45, 0x56, 0x4f, 0x4b, + 0x45, 0x10, 0x0a, 0x12, 0x14, 0x0a, 0x10, 0x57, 0x49, 0x54, 0x4e, 0x45, 0x53, 0x53, 0x5f, 0x4b, + 0x45, 0x59, 0x5f, 0x48, 0x41, 0x53, 0x48, 0x10, 0x0b, 0x12, 0x1b, 0x0a, 0x17, 0x4e, 0x45, 0x53, + 0x54, 0x45, 0x44, 0x5f, 0x57, 0x49, 0x54, 0x4e, 0x45, 0x53, 0x53, 0x5f, 0x4b, 0x45, 0x59, 0x5f, + 0x48, 0x41, 0x53, 0x48, 0x10, 0x0c, 0x12, 0x15, 0x0a, 0x11, 0x43, 0x4f, 0x4d, 0x4d, 0x49, 0x54, + 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x41, 0x4e, 0x43, 0x48, 0x4f, 0x52, 0x10, 0x0d, 0x32, 0xf7, 0x0b, + 0x0a, 0x09, 0x57, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x4b, 0x69, 0x74, 0x12, 0x4c, 0x0a, 0x0b, 0x4c, + 0x69, 0x73, 0x74, 0x55, 0x6e, 0x73, 0x70, 0x65, 0x6e, 0x74, 0x12, 0x1d, 0x2e, 0x77, 0x61, 0x6c, + 0x6c, 0x65, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x6e, 0x73, 0x70, 0x65, + 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x77, 0x61, 0x6c, 0x6c, + 0x65, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x6e, 0x73, 0x70, 0x65, 0x6e, + 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x0b, 0x4c, 0x65, 0x61, + 0x73, 0x65, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x12, 0x1d, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, + 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x65, 0x61, 0x73, 0x65, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, - 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x0b, 0x45, 0x73, 0x74, 0x69, 0x6d, - 0x61, 0x74, 0x65, 0x46, 0x65, 0x65, 0x12, 0x1d, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, - 0x70, 0x63, 0x2e, 0x45, 0x73, 0x74, 0x69, 0x6d, 0x61, 0x74, 0x65, 0x46, 0x65, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, 0x70, - 0x63, 0x2e, 0x45, 0x73, 0x74, 0x69, 0x6d, 0x61, 0x74, 0x65, 0x46, 0x65, 0x65, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x52, 0x0a, 0x0d, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, - 0x53, 0x77, 0x65, 0x65, 0x70, 0x73, 0x12, 0x1f, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, - 0x70, 0x63, 0x2e, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x53, 0x77, 0x65, 0x65, 0x70, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, - 0x72, 0x70, 0x63, 0x2e, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x53, 0x77, 0x65, 0x65, 0x70, - 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a, 0x07, 0x42, 0x75, 0x6d, - 0x70, 0x46, 0x65, 0x65, 0x12, 0x19, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, 0x70, 0x63, - 0x2e, 0x42, 0x75, 0x6d, 0x70, 0x46, 0x65, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x1a, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x42, 0x75, 0x6d, 0x70, - 0x46, 0x65, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x49, 0x0a, 0x0a, 0x4c, - 0x69, 0x73, 0x74, 0x53, 0x77, 0x65, 0x65, 0x70, 0x73, 0x12, 0x1c, 0x2e, 0x77, 0x61, 0x6c, 0x6c, - 0x65, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x77, 0x65, 0x65, 0x70, 0x73, + 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x65, 0x61, 0x73, 0x65, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x52, 0x0a, 0x0d, 0x52, 0x65, 0x6c, 0x65, 0x61, + 0x73, 0x65, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x12, 0x1f, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, + 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x4f, 0x75, 0x74, 0x70, + 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x77, 0x61, 0x6c, 0x6c, + 0x65, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x4f, 0x75, 0x74, + 0x70, 0x75, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x49, 0x0a, 0x0a, 0x4c, + 0x69, 0x73, 0x74, 0x4c, 0x65, 0x61, 0x73, 0x65, 0x73, 0x12, 0x1c, 0x2e, 0x77, 0x61, 0x6c, 0x6c, + 0x65, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x65, 0x61, 0x73, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, - 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x77, 0x65, 0x65, 0x70, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5b, 0x0a, 0x10, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x54, - 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x22, 0x2e, 0x77, 0x61, 0x6c, - 0x6c, 0x65, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x54, 0x72, 0x61, 0x6e, - 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, - 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x61, 0x62, 0x65, 0x6c, - 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x08, 0x46, 0x75, 0x6e, 0x64, 0x50, 0x73, 0x62, 0x74, 0x12, - 0x1a, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x75, 0x6e, 0x64, - 0x50, 0x73, 0x62, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x77, 0x61, - 0x6c, 0x6c, 0x65, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x75, 0x6e, 0x64, 0x50, 0x73, 0x62, 0x74, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x0c, 0x46, 0x69, 0x6e, 0x61, - 0x6c, 0x69, 0x7a, 0x65, 0x50, 0x73, 0x62, 0x74, 0x12, 0x1e, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, - 0x74, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x50, 0x73, 0x62, - 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, - 0x74, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x50, 0x73, 0x62, - 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x31, 0x5a, 0x2f, 0x67, 0x69, 0x74, - 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, - 0x67, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2f, 0x6c, 0x6e, 0x64, 0x2f, 0x6c, 0x6e, 0x72, - 0x70, 0x63, 0x2f, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, + 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x65, 0x61, 0x73, 0x65, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3a, 0x0a, 0x0d, 0x44, 0x65, 0x72, 0x69, 0x76, 0x65, + 0x4e, 0x65, 0x78, 0x74, 0x4b, 0x65, 0x79, 0x12, 0x11, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, + 0x72, 0x70, 0x63, 0x2e, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x1a, 0x16, 0x2e, 0x73, 0x69, 0x67, + 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4b, 0x65, 0x79, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x6f, 0x72, 0x12, 0x38, 0x0a, 0x09, 0x44, 0x65, 0x72, 0x69, 0x76, 0x65, 0x4b, 0x65, 0x79, 0x12, + 0x13, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4b, 0x65, 0x79, 0x4c, 0x6f, 0x63, + 0x61, 0x74, 0x6f, 0x72, 0x1a, 0x16, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4b, + 0x65, 0x79, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x12, 0x3b, 0x0a, 0x08, + 0x4e, 0x65, 0x78, 0x74, 0x41, 0x64, 0x64, 0x72, 0x12, 0x16, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, + 0x74, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x17, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, + 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x0c, 0x4c, 0x69, 0x73, + 0x74, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x12, 0x1e, 0x2e, 0x77, 0x61, 0x6c, 0x6c, + 0x65, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, + 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x77, 0x61, 0x6c, 0x6c, + 0x65, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, + 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x52, 0x0a, 0x0d, 0x49, 0x6d, + 0x70, 0x6f, 0x72, 0x74, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1f, 0x2e, 0x77, 0x61, + 0x6c, 0x6c, 0x65, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x41, 0x63, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x77, + 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x41, + 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x58, + 0x0a, 0x0f, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, + 0x79, 0x12, 0x21, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6d, + 0x70, 0x6f, 0x72, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, 0x70, 0x63, + 0x2e, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x48, 0x0a, 0x12, 0x50, 0x75, 0x62, 0x6c, + 0x69, 0x73, 0x68, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x16, + 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, + 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0x1a, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, + 0x70, 0x63, 0x2e, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x0b, 0x53, 0x65, 0x6e, 0x64, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, + 0x73, 0x12, 0x1d, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, + 0x6e, 0x64, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x1e, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6e, + 0x64, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x4c, 0x0a, 0x0b, 0x45, 0x73, 0x74, 0x69, 0x6d, 0x61, 0x74, 0x65, 0x46, 0x65, 0x65, 0x12, + 0x1d, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x45, 0x73, 0x74, 0x69, + 0x6d, 0x61, 0x74, 0x65, 0x46, 0x65, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, + 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x45, 0x73, 0x74, 0x69, 0x6d, + 0x61, 0x74, 0x65, 0x46, 0x65, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x52, + 0x0a, 0x0d, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x53, 0x77, 0x65, 0x65, 0x70, 0x73, 0x12, + 0x1f, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x65, 0x6e, 0x64, + 0x69, 0x6e, 0x67, 0x53, 0x77, 0x65, 0x65, 0x70, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x20, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x65, 0x6e, + 0x64, 0x69, 0x6e, 0x67, 0x53, 0x77, 0x65, 0x65, 0x70, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x40, 0x0a, 0x07, 0x42, 0x75, 0x6d, 0x70, 0x46, 0x65, 0x65, 0x12, 0x19, 0x2e, + 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x42, 0x75, 0x6d, 0x70, 0x46, 0x65, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, + 0x74, 0x72, 0x70, 0x63, 0x2e, 0x42, 0x75, 0x6d, 0x70, 0x46, 0x65, 0x65, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x49, 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x77, 0x65, 0x65, + 0x70, 0x73, 0x12, 0x1c, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, + 0x69, 0x73, 0x74, 0x53, 0x77, 0x65, 0x65, 0x70, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x1d, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x69, 0x73, + 0x74, 0x53, 0x77, 0x65, 0x65, 0x70, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x5b, 0x0a, 0x10, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x22, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, 0x70, 0x63, 0x2e, + 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, + 0x72, 0x70, 0x63, 0x2e, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x08, + 0x46, 0x75, 0x6e, 0x64, 0x50, 0x73, 0x62, 0x74, 0x12, 0x1a, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, + 0x74, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x75, 0x6e, 0x64, 0x50, 0x73, 0x62, 0x74, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, 0x70, 0x63, + 0x2e, 0x46, 0x75, 0x6e, 0x64, 0x50, 0x73, 0x62, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x43, 0x0a, 0x08, 0x53, 0x69, 0x67, 0x6e, 0x50, 0x73, 0x62, 0x74, 0x12, 0x1a, 0x2e, + 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x50, 0x73, + 0x62, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x77, 0x61, 0x6c, 0x6c, + 0x65, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x50, 0x73, 0x62, 0x74, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x0c, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, + 0x7a, 0x65, 0x50, 0x73, 0x62, 0x74, 0x12, 0x1e, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, + 0x70, 0x63, 0x2e, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x50, 0x73, 0x62, 0x74, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, + 0x70, 0x63, 0x2e, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x50, 0x73, 0x62, 0x74, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x31, 0x5a, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6e, + 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2f, 0x6c, 0x6e, 0x64, 0x2f, 0x6c, 0x6e, 0x72, 0x70, 0x63, + 0x2f, 0x77, 0x61, 0x6c, 0x6c, 0x65, 0x74, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, } var ( @@ -3231,7 +3340,7 @@ func file_walletrpc_walletkit_proto_rawDescGZIP() []byte { } var file_walletrpc_walletkit_proto_enumTypes = make([]protoimpl.EnumInfo, 2) -var file_walletrpc_walletkit_proto_msgTypes = make([]protoimpl.MessageInfo, 41) +var file_walletrpc_walletkit_proto_msgTypes = make([]protoimpl.MessageInfo, 43) var file_walletrpc_walletkit_proto_goTypes = []interface{}{ (AddressType)(0), // 0: walletrpc.AddressType (WitnessType)(0), // 1: walletrpc.WitnessType @@ -3270,23 +3379,25 @@ var file_walletrpc_walletkit_proto_goTypes = []interface{}{ (*FundPsbtResponse)(nil), // 34: walletrpc.FundPsbtResponse (*TxTemplate)(nil), // 35: walletrpc.TxTemplate (*UtxoLease)(nil), // 36: walletrpc.UtxoLease - (*FinalizePsbtRequest)(nil), // 37: walletrpc.FinalizePsbtRequest - (*FinalizePsbtResponse)(nil), // 38: walletrpc.FinalizePsbtResponse - (*ListLeasesRequest)(nil), // 39: walletrpc.ListLeasesRequest - (*ListLeasesResponse)(nil), // 40: walletrpc.ListLeasesResponse - (*ListSweepsResponse_TransactionIDs)(nil), // 41: walletrpc.ListSweepsResponse.TransactionIDs - nil, // 42: walletrpc.TxTemplate.OutputsEntry - (*lnrpc.Utxo)(nil), // 43: lnrpc.Utxo - (*lnrpc.OutPoint)(nil), // 44: lnrpc.OutPoint - (*signrpc.TxOut)(nil), // 45: signrpc.TxOut - (*lnrpc.TransactionDetails)(nil), // 46: lnrpc.TransactionDetails - (*signrpc.KeyLocator)(nil), // 47: signrpc.KeyLocator - (*signrpc.KeyDescriptor)(nil), // 48: signrpc.KeyDescriptor + (*SignPsbtRequest)(nil), // 37: walletrpc.SignPsbtRequest + (*SignPsbtResponse)(nil), // 38: walletrpc.SignPsbtResponse + (*FinalizePsbtRequest)(nil), // 39: walletrpc.FinalizePsbtRequest + (*FinalizePsbtResponse)(nil), // 40: walletrpc.FinalizePsbtResponse + (*ListLeasesRequest)(nil), // 41: walletrpc.ListLeasesRequest + (*ListLeasesResponse)(nil), // 42: walletrpc.ListLeasesResponse + (*ListSweepsResponse_TransactionIDs)(nil), // 43: walletrpc.ListSweepsResponse.TransactionIDs + nil, // 44: walletrpc.TxTemplate.OutputsEntry + (*lnrpc.Utxo)(nil), // 45: lnrpc.Utxo + (*lnrpc.OutPoint)(nil), // 46: lnrpc.OutPoint + (*signrpc.TxOut)(nil), // 47: signrpc.TxOut + (*lnrpc.TransactionDetails)(nil), // 48: lnrpc.TransactionDetails + (*signrpc.KeyLocator)(nil), // 49: signrpc.KeyLocator + (*signrpc.KeyDescriptor)(nil), // 50: signrpc.KeyDescriptor } var file_walletrpc_walletkit_proto_depIdxs = []int32{ - 43, // 0: walletrpc.ListUnspentResponse.utxos:type_name -> lnrpc.Utxo - 44, // 1: walletrpc.LeaseOutputRequest.outpoint:type_name -> lnrpc.OutPoint - 44, // 2: walletrpc.ReleaseOutputRequest.outpoint:type_name -> lnrpc.OutPoint + 45, // 0: walletrpc.ListUnspentResponse.utxos:type_name -> lnrpc.Utxo + 46, // 1: walletrpc.LeaseOutputRequest.outpoint:type_name -> lnrpc.OutPoint + 46, // 2: walletrpc.ReleaseOutputRequest.outpoint:type_name -> lnrpc.OutPoint 0, // 3: walletrpc.AddrRequest.type:type_name -> walletrpc.AddressType 0, // 4: walletrpc.Account.address_type:type_name -> walletrpc.AddressType 0, // 5: walletrpc.ListAccountsRequest.address_type:type_name -> walletrpc.AddressType @@ -3294,25 +3405,25 @@ var file_walletrpc_walletkit_proto_depIdxs = []int32{ 0, // 7: walletrpc.ImportAccountRequest.address_type:type_name -> walletrpc.AddressType 11, // 8: walletrpc.ImportAccountResponse.account:type_name -> walletrpc.Account 0, // 9: walletrpc.ImportPublicKeyRequest.address_type:type_name -> walletrpc.AddressType - 45, // 10: walletrpc.SendOutputsRequest.outputs:type_name -> signrpc.TxOut - 44, // 11: walletrpc.PendingSweep.outpoint:type_name -> lnrpc.OutPoint + 47, // 10: walletrpc.SendOutputsRequest.outputs:type_name -> signrpc.TxOut + 46, // 11: walletrpc.PendingSweep.outpoint:type_name -> lnrpc.OutPoint 1, // 12: walletrpc.PendingSweep.witness_type:type_name -> walletrpc.WitnessType 24, // 13: walletrpc.PendingSweepsResponse.pending_sweeps:type_name -> walletrpc.PendingSweep - 44, // 14: walletrpc.BumpFeeRequest.outpoint:type_name -> lnrpc.OutPoint - 46, // 15: walletrpc.ListSweepsResponse.transaction_details:type_name -> lnrpc.TransactionDetails - 41, // 16: walletrpc.ListSweepsResponse.transaction_ids:type_name -> walletrpc.ListSweepsResponse.TransactionIDs + 46, // 14: walletrpc.BumpFeeRequest.outpoint:type_name -> lnrpc.OutPoint + 48, // 15: walletrpc.ListSweepsResponse.transaction_details:type_name -> lnrpc.TransactionDetails + 43, // 16: walletrpc.ListSweepsResponse.transaction_ids:type_name -> walletrpc.ListSweepsResponse.TransactionIDs 35, // 17: walletrpc.FundPsbtRequest.raw:type_name -> walletrpc.TxTemplate 36, // 18: walletrpc.FundPsbtResponse.locked_utxos:type_name -> walletrpc.UtxoLease - 44, // 19: walletrpc.TxTemplate.inputs:type_name -> lnrpc.OutPoint - 42, // 20: walletrpc.TxTemplate.outputs:type_name -> walletrpc.TxTemplate.OutputsEntry - 44, // 21: walletrpc.UtxoLease.outpoint:type_name -> lnrpc.OutPoint + 46, // 19: walletrpc.TxTemplate.inputs:type_name -> lnrpc.OutPoint + 44, // 20: walletrpc.TxTemplate.outputs:type_name -> walletrpc.TxTemplate.OutputsEntry + 46, // 21: walletrpc.UtxoLease.outpoint:type_name -> lnrpc.OutPoint 36, // 22: walletrpc.ListLeasesResponse.locked_utxos:type_name -> walletrpc.UtxoLease 2, // 23: walletrpc.WalletKit.ListUnspent:input_type -> walletrpc.ListUnspentRequest 4, // 24: walletrpc.WalletKit.LeaseOutput:input_type -> walletrpc.LeaseOutputRequest 6, // 25: walletrpc.WalletKit.ReleaseOutput:input_type -> walletrpc.ReleaseOutputRequest - 39, // 26: walletrpc.WalletKit.ListLeases:input_type -> walletrpc.ListLeasesRequest + 41, // 26: walletrpc.WalletKit.ListLeases:input_type -> walletrpc.ListLeasesRequest 8, // 27: walletrpc.WalletKit.DeriveNextKey:input_type -> walletrpc.KeyReq - 47, // 28: walletrpc.WalletKit.DeriveKey:input_type -> signrpc.KeyLocator + 49, // 28: walletrpc.WalletKit.DeriveKey:input_type -> signrpc.KeyLocator 9, // 29: walletrpc.WalletKit.NextAddr:input_type -> walletrpc.AddrRequest 12, // 30: walletrpc.WalletKit.ListAccounts:input_type -> walletrpc.ListAccountsRequest 14, // 31: walletrpc.WalletKit.ImportAccount:input_type -> walletrpc.ImportAccountRequest @@ -3325,28 +3436,30 @@ var file_walletrpc_walletkit_proto_depIdxs = []int32{ 29, // 38: walletrpc.WalletKit.ListSweeps:input_type -> walletrpc.ListSweepsRequest 31, // 39: walletrpc.WalletKit.LabelTransaction:input_type -> walletrpc.LabelTransactionRequest 33, // 40: walletrpc.WalletKit.FundPsbt:input_type -> walletrpc.FundPsbtRequest - 37, // 41: walletrpc.WalletKit.FinalizePsbt:input_type -> walletrpc.FinalizePsbtRequest - 3, // 42: walletrpc.WalletKit.ListUnspent:output_type -> walletrpc.ListUnspentResponse - 5, // 43: walletrpc.WalletKit.LeaseOutput:output_type -> walletrpc.LeaseOutputResponse - 7, // 44: walletrpc.WalletKit.ReleaseOutput:output_type -> walletrpc.ReleaseOutputResponse - 40, // 45: walletrpc.WalletKit.ListLeases:output_type -> walletrpc.ListLeasesResponse - 48, // 46: walletrpc.WalletKit.DeriveNextKey:output_type -> signrpc.KeyDescriptor - 48, // 47: walletrpc.WalletKit.DeriveKey:output_type -> signrpc.KeyDescriptor - 10, // 48: walletrpc.WalletKit.NextAddr:output_type -> walletrpc.AddrResponse - 13, // 49: walletrpc.WalletKit.ListAccounts:output_type -> walletrpc.ListAccountsResponse - 15, // 50: walletrpc.WalletKit.ImportAccount:output_type -> walletrpc.ImportAccountResponse - 17, // 51: walletrpc.WalletKit.ImportPublicKey:output_type -> walletrpc.ImportPublicKeyResponse - 19, // 52: walletrpc.WalletKit.PublishTransaction:output_type -> walletrpc.PublishResponse - 21, // 53: walletrpc.WalletKit.SendOutputs:output_type -> walletrpc.SendOutputsResponse - 23, // 54: walletrpc.WalletKit.EstimateFee:output_type -> walletrpc.EstimateFeeResponse - 26, // 55: walletrpc.WalletKit.PendingSweeps:output_type -> walletrpc.PendingSweepsResponse - 28, // 56: walletrpc.WalletKit.BumpFee:output_type -> walletrpc.BumpFeeResponse - 30, // 57: walletrpc.WalletKit.ListSweeps:output_type -> walletrpc.ListSweepsResponse - 32, // 58: walletrpc.WalletKit.LabelTransaction:output_type -> walletrpc.LabelTransactionResponse - 34, // 59: walletrpc.WalletKit.FundPsbt:output_type -> walletrpc.FundPsbtResponse - 38, // 60: walletrpc.WalletKit.FinalizePsbt:output_type -> walletrpc.FinalizePsbtResponse - 42, // [42:61] is the sub-list for method output_type - 23, // [23:42] is the sub-list for method input_type + 37, // 41: walletrpc.WalletKit.SignPsbt:input_type -> walletrpc.SignPsbtRequest + 39, // 42: walletrpc.WalletKit.FinalizePsbt:input_type -> walletrpc.FinalizePsbtRequest + 3, // 43: walletrpc.WalletKit.ListUnspent:output_type -> walletrpc.ListUnspentResponse + 5, // 44: walletrpc.WalletKit.LeaseOutput:output_type -> walletrpc.LeaseOutputResponse + 7, // 45: walletrpc.WalletKit.ReleaseOutput:output_type -> walletrpc.ReleaseOutputResponse + 42, // 46: walletrpc.WalletKit.ListLeases:output_type -> walletrpc.ListLeasesResponse + 50, // 47: walletrpc.WalletKit.DeriveNextKey:output_type -> signrpc.KeyDescriptor + 50, // 48: walletrpc.WalletKit.DeriveKey:output_type -> signrpc.KeyDescriptor + 10, // 49: walletrpc.WalletKit.NextAddr:output_type -> walletrpc.AddrResponse + 13, // 50: walletrpc.WalletKit.ListAccounts:output_type -> walletrpc.ListAccountsResponse + 15, // 51: walletrpc.WalletKit.ImportAccount:output_type -> walletrpc.ImportAccountResponse + 17, // 52: walletrpc.WalletKit.ImportPublicKey:output_type -> walletrpc.ImportPublicKeyResponse + 19, // 53: walletrpc.WalletKit.PublishTransaction:output_type -> walletrpc.PublishResponse + 21, // 54: walletrpc.WalletKit.SendOutputs:output_type -> walletrpc.SendOutputsResponse + 23, // 55: walletrpc.WalletKit.EstimateFee:output_type -> walletrpc.EstimateFeeResponse + 26, // 56: walletrpc.WalletKit.PendingSweeps:output_type -> walletrpc.PendingSweepsResponse + 28, // 57: walletrpc.WalletKit.BumpFee:output_type -> walletrpc.BumpFeeResponse + 30, // 58: walletrpc.WalletKit.ListSweeps:output_type -> walletrpc.ListSweepsResponse + 32, // 59: walletrpc.WalletKit.LabelTransaction:output_type -> walletrpc.LabelTransactionResponse + 34, // 60: walletrpc.WalletKit.FundPsbt:output_type -> walletrpc.FundPsbtResponse + 38, // 61: walletrpc.WalletKit.SignPsbt:output_type -> walletrpc.SignPsbtResponse + 40, // 62: walletrpc.WalletKit.FinalizePsbt:output_type -> walletrpc.FinalizePsbtResponse + 43, // [43:63] is the sub-list for method output_type + 23, // [23:43] is the sub-list for method input_type 23, // [23:23] is the sub-list for extension type_name 23, // [23:23] is the sub-list for extension extendee 0, // [0:23] is the sub-list for field type_name @@ -3779,7 +3892,7 @@ func file_walletrpc_walletkit_proto_init() { } } file_walletrpc_walletkit_proto_msgTypes[35].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*FinalizePsbtRequest); i { + switch v := v.(*SignPsbtRequest); i { case 0: return &v.state case 1: @@ -3791,7 +3904,7 @@ func file_walletrpc_walletkit_proto_init() { } } file_walletrpc_walletkit_proto_msgTypes[36].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*FinalizePsbtResponse); i { + switch v := v.(*SignPsbtResponse); i { case 0: return &v.state case 1: @@ -3803,7 +3916,7 @@ func file_walletrpc_walletkit_proto_init() { } } file_walletrpc_walletkit_proto_msgTypes[37].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListLeasesRequest); i { + switch v := v.(*FinalizePsbtRequest); i { case 0: return &v.state case 1: @@ -3815,7 +3928,7 @@ func file_walletrpc_walletkit_proto_init() { } } file_walletrpc_walletkit_proto_msgTypes[38].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListLeasesResponse); i { + switch v := v.(*FinalizePsbtResponse); i { case 0: return &v.state case 1: @@ -3827,6 +3940,30 @@ func file_walletrpc_walletkit_proto_init() { } } file_walletrpc_walletkit_proto_msgTypes[39].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListLeasesRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_walletrpc_walletkit_proto_msgTypes[40].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListLeasesResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_walletrpc_walletkit_proto_msgTypes[41].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ListSweepsResponse_TransactionIDs); i { case 0: return &v.state @@ -3855,7 +3992,7 @@ func file_walletrpc_walletkit_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_walletrpc_walletkit_proto_rawDesc, NumEnums: 2, - NumMessages: 41, + NumMessages: 43, NumExtensions: 0, NumServices: 1, }, diff --git a/lnrpc/walletrpc/walletkit.pb.gw.go b/lnrpc/walletrpc/walletkit.pb.gw.go index f8da00bb8..08566ef21 100644 --- a/lnrpc/walletrpc/walletkit.pb.gw.go +++ b/lnrpc/walletrpc/walletkit.pb.gw.go @@ -634,6 +634,40 @@ func local_request_WalletKit_FundPsbt_0(ctx context.Context, marshaler runtime.M } +func request_WalletKit_SignPsbt_0(ctx context.Context, marshaler runtime.Marshaler, client WalletKitClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq SignPsbtRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := client.SignPsbt(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_WalletKit_SignPsbt_0(ctx context.Context, marshaler runtime.Marshaler, server WalletKitServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq SignPsbtRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + msg, err := server.SignPsbt(ctx, &protoReq) + return msg, metadata, err + +} + func request_WalletKit_FinalizePsbt_0(ctx context.Context, marshaler runtime.Marshaler, client WalletKitClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq FinalizePsbtRequest var metadata runtime.ServerMetadata @@ -1088,6 +1122,29 @@ func RegisterWalletKitHandlerServer(ctx context.Context, mux *runtime.ServeMux, }) + mux.Handle("POST", pattern_WalletKit_SignPsbt_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/walletrpc.WalletKit/SignPsbt", runtime.WithHTTPPathPattern("/v2/wallet/psbt/sign")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_WalletKit_SignPsbt_0(rctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_WalletKit_SignPsbt_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + mux.Handle("POST", pattern_WalletKit_FinalizePsbt_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -1512,6 +1569,26 @@ func RegisterWalletKitHandlerClient(ctx context.Context, mux *runtime.ServeMux, }) + mux.Handle("POST", pattern_WalletKit_SignPsbt_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req, "/walletrpc.WalletKit/SignPsbt", runtime.WithHTTPPathPattern("/v2/wallet/psbt/sign")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_WalletKit_SignPsbt_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_WalletKit_SignPsbt_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + mux.Handle("POST", pattern_WalletKit_FinalizePsbt_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -1572,6 +1649,8 @@ var ( pattern_WalletKit_FundPsbt_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v2", "wallet", "psbt", "fund"}, "")) + pattern_WalletKit_SignPsbt_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v2", "wallet", "psbt", "sign"}, "")) + pattern_WalletKit_FinalizePsbt_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v2", "wallet", "psbt", "finalize"}, "")) ) @@ -1612,5 +1691,7 @@ var ( forward_WalletKit_FundPsbt_0 = runtime.ForwardResponseMessage + forward_WalletKit_SignPsbt_0 = runtime.ForwardResponseMessage + forward_WalletKit_FinalizePsbt_0 = runtime.ForwardResponseMessage ) diff --git a/lnrpc/walletrpc/walletkit.pb.json.go b/lnrpc/walletrpc/walletkit.pb.json.go index 9eb5c69e4..8d8b73111 100644 --- a/lnrpc/walletrpc/walletkit.pb.json.go +++ b/lnrpc/walletrpc/walletkit.pb.json.go @@ -474,6 +474,31 @@ func RegisterWalletKitJSONCallbacks(registry map[string]func(ctx context.Context callback(string(respBytes), nil) } + registry["walletrpc.WalletKit.SignPsbt"] = func(ctx context.Context, + conn *grpc.ClientConn, reqJSON string, callback func(string, error)) { + + req := &SignPsbtRequest{} + err := marshaler.Unmarshal([]byte(reqJSON), req) + if err != nil { + callback("", err) + return + } + + client := NewWalletKitClient(conn) + resp, err := client.SignPsbt(ctx, req) + if err != nil { + callback("", err) + return + } + + respBytes, err := marshaler.Marshal(resp) + if err != nil { + callback("", err) + return + } + callback(string(respBytes), nil) + } + registry["walletrpc.WalletKit.FinalizePsbt"] = func(ctx context.Context, conn *grpc.ClientConn, reqJSON string, callback func(string, error)) { diff --git a/lnrpc/walletrpc/walletkit.proto b/lnrpc/walletrpc/walletkit.proto index 2d552915e..84ff7a688 100644 --- a/lnrpc/walletrpc/walletkit.proto +++ b/lnrpc/walletrpc/walletkit.proto @@ -200,6 +200,21 @@ service WalletKit { */ rpc FundPsbt (FundPsbtRequest) returns (FundPsbtResponse); + /* + SignPsbt expects a partial transaction with all inputs and outputs fully + declared and tries to sign all unsigned inputs that have all required fields + (UTXO information, BIP32 derivation information, witness or sig scripts) + set. + If no error is returned, the PSBT is ready to be given to the next signer or + to be finalized if lnd was the last signer. + + NOTE: This RPC only signs inputs (and only those it can sign), it does not + perform any other tasks (such as coin selection, UTXO locking or + input/output/fee value validation, PSBT finalization). Any input that is + incomplete will be skipped. + */ + rpc SignPsbt (SignPsbtRequest) returns (SignPsbtResponse); + /* FinalizePsbt expects a partial transaction with all inputs and outputs fully declared and tries to sign all inputs that belong to the wallet. Lnd must be @@ -820,6 +835,19 @@ message UtxoLease { uint64 expiration = 3; } +message SignPsbtRequest { + /* + The PSBT that should be signed. The PSBT must contain all required inputs, + outputs, UTXO data and custom fields required to identify the signing key. + */ + bytes funded_psbt = 1; +} + +message SignPsbtResponse { + // The signed transaction in PSBT format. + bytes signed_psbt = 1; +} + message FinalizePsbtRequest { /* A PSBT that should be signed and finalized. The PSBT must contain all diff --git a/lnrpc/walletrpc/walletkit.swagger.json b/lnrpc/walletrpc/walletkit.swagger.json index 922513858..c40c66691 100644 --- a/lnrpc/walletrpc/walletkit.swagger.json +++ b/lnrpc/walletrpc/walletkit.swagger.json @@ -364,6 +364,40 @@ ] } }, + "/v2/wallet/psbt/sign": { + "post": { + "summary": "SignPsbt expects a partial transaction with all inputs and outputs fully\ndeclared and tries to sign all unsigned inputs that have all required fields\n(UTXO information, BIP32 derivation information, witness or sig scripts)\nset.\nIf no error is returned, the PSBT is ready to be given to the next signer or\nto be finalized if lnd was the last signer.", + "description": "NOTE: This RPC only signs inputs (and only those it can sign), it does not\nperform any other tasks (such as coin selection, UTXO locking or\ninput/output/fee value validation, PSBT finalization). Any input that is\nincomplete will be skipped.", + "operationId": "WalletKit_SignPsbt", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/walletrpcSignPsbtResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/walletrpcSignPsbtRequest" + } + } + ], + "tags": [ + "WalletKit" + ] + } + }, "/v2/wallet/send": { "post": { "summary": "SendOutputs is similar to the existing sendmany call in Bitcoind, and\nallows the caller to create a transaction that sends to several outputs at\nonce. This is ideal when wanting to batch create a set of transactions.", @@ -1392,6 +1426,26 @@ } } }, + "walletrpcSignPsbtRequest": { + "type": "object", + "properties": { + "funded_psbt": { + "type": "string", + "format": "byte", + "description": "The PSBT that should be signed. The PSBT must contain all required inputs,\noutputs, UTXO data and custom fields required to identify the signing key." + } + } + }, + "walletrpcSignPsbtResponse": { + "type": "object", + "properties": { + "signed_psbt": { + "type": "string", + "format": "byte", + "description": "The signed transaction in PSBT format." + } + } + }, "walletrpcTransaction": { "type": "object", "properties": { diff --git a/lnrpc/walletrpc/walletkit.yaml b/lnrpc/walletrpc/walletkit.yaml index 7ad00c0f4..dc9a211fe 100644 --- a/lnrpc/walletrpc/walletkit.yaml +++ b/lnrpc/walletrpc/walletkit.yaml @@ -47,6 +47,9 @@ http: - selector: walletrpc.WalletKit.FundPsbt post: "/v2/wallet/psbt/fund" body: "*" + - selector: walletrpc.WalletKit.SignPsbt + post: "/v2/wallet/psbt/sign" + body: "*" - selector: walletrpc.WalletKit.FinalizePsbt post: "/v2/wallet/psbt/finalize" body: "*" diff --git a/lnrpc/walletrpc/walletkit_grpc.pb.go b/lnrpc/walletrpc/walletkit_grpc.pb.go index d73d095ac..b61229945 100644 --- a/lnrpc/walletrpc/walletkit_grpc.pb.go +++ b/lnrpc/walletrpc/walletkit_grpc.pb.go @@ -172,6 +172,19 @@ type WalletKitClient interface { //an error on the caller's side. FundPsbt(ctx context.Context, in *FundPsbtRequest, opts ...grpc.CallOption) (*FundPsbtResponse, error) // + //SignPsbt expects a partial transaction with all inputs and outputs fully + //declared and tries to sign all unsigned inputs that have all required fields + //(UTXO information, BIP32 derivation information, witness or sig scripts) + //set. + //If no error is returned, the PSBT is ready to be given to the next signer or + //to be finalized if lnd was the last signer. + // + //NOTE: This RPC only signs inputs (and only those it can sign), it does not + //perform any other tasks (such as coin selection, UTXO locking or + //input/output/fee value validation, PSBT finalization). Any input that is + //incomplete will be skipped. + SignPsbt(ctx context.Context, in *SignPsbtRequest, opts ...grpc.CallOption) (*SignPsbtResponse, error) + // //FinalizePsbt expects a partial transaction with all inputs and outputs fully //declared and tries to sign all inputs that belong to the wallet. Lnd must be //the last signer of the transaction. That means, if there are any unsigned @@ -356,6 +369,15 @@ func (c *walletKitClient) FundPsbt(ctx context.Context, in *FundPsbtRequest, opt return out, nil } +func (c *walletKitClient) SignPsbt(ctx context.Context, in *SignPsbtRequest, opts ...grpc.CallOption) (*SignPsbtResponse, error) { + out := new(SignPsbtResponse) + err := c.cc.Invoke(ctx, "/walletrpc.WalletKit/SignPsbt", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *walletKitClient) FinalizePsbt(ctx context.Context, in *FinalizePsbtRequest, opts ...grpc.CallOption) (*FinalizePsbtResponse, error) { out := new(FinalizePsbtResponse) err := c.cc.Invoke(ctx, "/walletrpc.WalletKit/FinalizePsbt", in, out, opts...) @@ -522,6 +544,19 @@ type WalletKitServer interface { //an error on the caller's side. FundPsbt(context.Context, *FundPsbtRequest) (*FundPsbtResponse, error) // + //SignPsbt expects a partial transaction with all inputs and outputs fully + //declared and tries to sign all unsigned inputs that have all required fields + //(UTXO information, BIP32 derivation information, witness or sig scripts) + //set. + //If no error is returned, the PSBT is ready to be given to the next signer or + //to be finalized if lnd was the last signer. + // + //NOTE: This RPC only signs inputs (and only those it can sign), it does not + //perform any other tasks (such as coin selection, UTXO locking or + //input/output/fee value validation, PSBT finalization). Any input that is + //incomplete will be skipped. + SignPsbt(context.Context, *SignPsbtRequest) (*SignPsbtResponse, error) + // //FinalizePsbt expects a partial transaction with all inputs and outputs fully //declared and tries to sign all inputs that belong to the wallet. Lnd must be //the last signer of the transaction. That means, if there are any unsigned @@ -595,6 +630,9 @@ func (UnimplementedWalletKitServer) LabelTransaction(context.Context, *LabelTran func (UnimplementedWalletKitServer) FundPsbt(context.Context, *FundPsbtRequest) (*FundPsbtResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method FundPsbt not implemented") } +func (UnimplementedWalletKitServer) SignPsbt(context.Context, *SignPsbtRequest) (*SignPsbtResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method SignPsbt not implemented") +} func (UnimplementedWalletKitServer) FinalizePsbt(context.Context, *FinalizePsbtRequest) (*FinalizePsbtResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method FinalizePsbt not implemented") } @@ -935,6 +973,24 @@ func _WalletKit_FundPsbt_Handler(srv interface{}, ctx context.Context, dec func( return interceptor(ctx, in, info, handler) } +func _WalletKit_SignPsbt_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SignPsbtRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(WalletKitServer).SignPsbt(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/walletrpc.WalletKit/SignPsbt", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(WalletKitServer).SignPsbt(ctx, req.(*SignPsbtRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _WalletKit_FinalizePsbt_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(FinalizePsbtRequest) if err := dec(in); err != nil { @@ -1032,6 +1088,10 @@ var WalletKit_ServiceDesc = grpc.ServiceDesc{ MethodName: "FundPsbt", Handler: _WalletKit_FundPsbt_Handler, }, + { + MethodName: "SignPsbt", + Handler: _WalletKit_SignPsbt_Handler, + }, { MethodName: "FinalizePsbt", Handler: _WalletKit_FinalizePsbt_Handler, diff --git a/lnrpc/walletrpc/walletkit_server.go b/lnrpc/walletrpc/walletkit_server.go index 31796972f..d0e807c6d 100644 --- a/lnrpc/walletrpc/walletkit_server.go +++ b/lnrpc/walletrpc/walletkit_server.go @@ -122,6 +122,10 @@ var ( Entity: "onchain", Action: "write", }}, + "/walletrpc.WalletKit/SignPsbt": {{ + Entity: "onchain", + Action: "write", + }}, "/walletrpc.WalletKit/FinalizePsbt": {{ Entity: "onchain", Action: "write", @@ -1188,6 +1192,47 @@ func marshallLeases(locks []*wtxmgr.LockedOutput) []*UtxoLease { return rpcLocks } +// SignPsbt expects a partial transaction with all inputs and outputs fully +// declared and tries to sign all unsigned inputs that have all required fields +// (UTXO information, BIP32 derivation information, witness or sig scripts) +// set. +// If no error is returned, the PSBT is ready to be given to the next signer or +// to be finalized if lnd was the last signer. +// +// NOTE: This RPC only signs inputs (and only those it can sign), it does not +// perform any other tasks (such as coin selection, UTXO locking or +// input/output/fee value validation, PSBT finalization). Any input that is +// incomplete will be skipped. +func (w *WalletKit) SignPsbt(_ context.Context, req *SignPsbtRequest) ( + *SignPsbtResponse, error) { + + packet, err := psbt.NewFromRawBytes( + bytes.NewReader(req.FundedPsbt), false, + ) + if err != nil { + return nil, fmt.Errorf("error parsing PSBT: %v", err) + } + + // Let the wallet do the heavy lifting. This will sign all inputs that + // we have the UTXO for. If some inputs can't be signed and don't have + // witness data attached, they will just be skipped. + err = w.cfg.Wallet.SignPsbt(packet) + if err != nil { + return nil, fmt.Errorf("error signing PSBT: %v", err) + } + + // Serialize the signed PSBT in both the packet and wire format. + var signedPsbtBytes bytes.Buffer + err = packet.Serialize(&signedPsbtBytes) + if err != nil { + return nil, fmt.Errorf("error serializing PSBT: %v", err) + } + + return &SignPsbtResponse{ + SignedPsbt: signedPsbtBytes.Bytes(), + }, nil +} + // FinalizePsbt expects a partial transaction with all inputs and outputs fully // declared and tries to sign all inputs that belong to the wallet. Lnd must be // the last signer of the transaction. That means, if there are any unsigned diff --git a/lntest/itest/lnd_psbt_test.go b/lntest/itest/lnd_psbt_test.go index 5dfdac776..5b4552600 100644 --- a/lntest/itest/lnd_psbt_test.go +++ b/lntest/itest/lnd_psbt_test.go @@ -6,12 +6,15 @@ import ( "crypto/rand" "fmt" + "github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcutil/hdkeychain" "github.com/btcsuite/btcutil/psbt" "github.com/lightningnetwork/lnd/funding" "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/signrpc" "github.com/lightningnetwork/lnd/lnrpc/walletrpc" "github.com/lightningnetwork/lnd/lntest" "github.com/stretchr/testify/require" @@ -40,6 +43,12 @@ func runPsbtChanFunding(net *lntest.NetworkHarness, t *harnessTest, carol, dave *lntest.HarnessNode) { ctxb := context.Background() + + // Everything we do here should be done within a second or two, so we + // can just keep a single timeout context around for all calls. + ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + const chanSize = funding.MaxBtcFundingAmount net.SendCoins(t.t, btcutil.SatoshiPerBitcoin, dave) @@ -63,8 +72,6 @@ func runPsbtChanFunding(net *lntest.NetworkHarness, t *harnessTest, carol, // Now that we have the pending channel ID, Carol will open the channel // by specifying a PSBT shim. We use the NoPublish flag here to avoid // publishing the whole batch TX too early. - ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) - defer cancel() chanUpdates, tempPsbt, err := openChannelPsbt( ctxt, carol, dave, lntest.OpenChannelParams{ Amt: chanSize, @@ -83,8 +90,6 @@ func runPsbtChanFunding(net *lntest.NetworkHarness, t *harnessTest, carol, // Let's add a second channel to the batch. This time between Carol and // Alice. We will publish the batch TX once this channel funding is // complete. - ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout) - defer cancel() chanUpdates2, psbtBytes2, err := openChannelPsbt( ctxt, carol, net.Alice, lntest.OpenChannelParams{ Amt: chanSize, @@ -104,8 +109,6 @@ func runPsbtChanFunding(net *lntest.NetworkHarness, t *harnessTest, carol, // We'll now ask Dave's wallet to fund the PSBT for us. This will return // a packet with inputs and outputs set but without any witness data. // This is exactly what we need for the next step. - ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout) - defer cancel() fundReq := &walletrpc.FundPsbtRequest{ Template: &walletrpc.FundPsbtRequest_Psbt{ Psbt: psbtBytes2, @@ -140,8 +143,6 @@ func runPsbtChanFunding(net *lntest.NetworkHarness, t *harnessTest, carol, // Now we'll ask Dave's wallet to sign the PSBT so we can finish the // funding flow. - ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout) - defer cancel() finalizeReq := &walletrpc.FinalizePsbtRequest{ FundedPsbt: fundResp.FundedPsbt, } @@ -161,8 +162,6 @@ func runPsbtChanFunding(net *lntest.NetworkHarness, t *harnessTest, carol, // Consume the "channel pending" update. This waits until the funding // transaction was fully compiled. - ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout) - defer cancel() updateResp, err := receiveChanUpdate(ctxt, chanUpdates) require.NoError(t.t, err) upd, ok := updateResp.Update.(*lnrpc.OpenStatusUpdate_ChanPending) @@ -195,8 +194,6 @@ func runPsbtChanFunding(net *lntest.NetworkHarness, t *harnessTest, carol, // Consume the "channel pending" update for the second channel. This // waits until the funding transaction was fully compiled and in this // case published. - ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout) - defer cancel() updateResp2, err := receiveChanUpdate(ctxt, chanUpdates2) require.NoError(t.t, err) upd2, ok := updateResp2.Update.(*lnrpc.OpenStatusUpdate_ChanPending) @@ -225,8 +222,6 @@ func runPsbtChanFunding(net *lntest.NetworkHarness, t *harnessTest, carol, // With the channel open, ensure that it is counted towards Carol's // total channel balance. balReq := &lnrpc.ChannelBalanceRequest{} - ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout) - defer cancel() balRes, err := carol.ChannelBalance(ctxt, balReq) require.NoError(t.t, err) require.NotEqual(t.t, int64(0), balRes.LocalBalance.Sat) @@ -238,7 +233,6 @@ func runPsbtChanFunding(net *lntest.NetworkHarness, t *harnessTest, carol, Memo: "new chans", Value: int64(payAmt), } - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) resp, err := dave.AddInvoice(ctxt, invoice) require.NoError(t.t, err) err = completePaymentRequests( @@ -261,6 +255,11 @@ func testPsbtChanFundingExternal(net *lntest.NetworkHarness, t *harnessTest) { ctxb := context.Background() const chanSize = funding.MaxBtcFundingAmount + // Everything we do here should be done within a second or two, so we + // can just keep a single timeout context around for all calls. + ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + // First, we'll create two new nodes that we'll use to open channels // between for this test. Both these nodes have an empty wallet as Alice // will be funding the channel. @@ -290,8 +289,6 @@ func testPsbtChanFundingExternal(net *lntest.NetworkHarness, t *harnessTest) { // Now that we have the pending channel ID, Carol will open the channel // by specifying a PSBT shim. We use the NoPublish flag here to avoid // publishing the whole batch TX too early. - ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) - defer cancel() chanUpdates, tempPsbt, err := openChannelPsbt( ctxt, carol, dave, lntest.OpenChannelParams{ Amt: chanSize, @@ -310,8 +307,6 @@ func testPsbtChanFundingExternal(net *lntest.NetworkHarness, t *harnessTest) { // Let's add a second channel to the batch. This time between Carol and // Alice. We will publish the batch TX once this channel funding is // complete. - ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout) - defer cancel() chanUpdates2, psbtBytes2, err := openChannelPsbt( ctxt, carol, net.Alice, lntest.OpenChannelParams{ Amt: chanSize, @@ -331,8 +326,6 @@ func testPsbtChanFundingExternal(net *lntest.NetworkHarness, t *harnessTest) { // We'll now ask Alice's wallet to fund the PSBT for us. This will // return a packet with inputs and outputs set but without any witness // data. This is exactly what we need for the next step. - ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout) - defer cancel() fundReq := &walletrpc.FundPsbtRequest{ Template: &walletrpc.FundPsbtRequest_Psbt{ Psbt: psbtBytes2, @@ -375,8 +368,6 @@ func testPsbtChanFundingExternal(net *lntest.NetworkHarness, t *harnessTest) { // Consume the "channel pending" update. This waits until the funding // transaction was fully compiled for both channels. - ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout) - defer cancel() updateResp, err := receiveChanUpdate(ctxt, chanUpdates) require.NoError(t.t, err) upd, ok := updateResp.Update.(*lnrpc.OpenStatusUpdate_ChanPending) @@ -403,8 +394,6 @@ func testPsbtChanFundingExternal(net *lntest.NetworkHarness, t *harnessTest) { // Now we'll ask Alice's wallet to sign the PSBT so we can finish the // funding flow. - ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout) - defer cancel() finalizeReq := &walletrpc.FinalizePsbtRequest{ FundedPsbt: fundResp.FundedPsbt, } @@ -439,8 +428,6 @@ func testPsbtChanFundingExternal(net *lntest.NetworkHarness, t *harnessTest) { // With the channel open, ensure that it is counted towards Carol's // total channel balance. balReq := &lnrpc.ChannelBalanceRequest{} - ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout) - defer cancel() balRes, err := carol.ChannelBalance(ctxt, balReq) require.NoError(t.t, err) require.NotEqual(t.t, int64(0), balRes.LocalBalance.Sat) @@ -452,7 +439,6 @@ func testPsbtChanFundingExternal(net *lntest.NetworkHarness, t *harnessTest) { Memo: "new chans", Value: int64(payAmt), } - ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) resp, err := dave.AddInvoice(ctxt, invoice) require.NoError(t.t, err) err = completePaymentRequests( @@ -476,6 +462,11 @@ func testPsbtChanFundingSingleStep(net *lntest.NetworkHarness, t *harnessTest) { ctxb := context.Background() const chanSize = funding.MaxBtcFundingAmount + // Everything we do here should be done within a second or two, so we + // can just keep a single timeout context around for all calls. + ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + args := nodeArgsForCommitType(lnrpc.CommitmentType_ANCHORS) // First, we'll create two new nodes that we'll use to open channels @@ -513,8 +504,6 @@ func testPsbtChanFundingSingleStep(net *lntest.NetworkHarness, t *harnessTest) { // Now that we have the pending channel ID, Carol will open the channel // by specifying a PSBT shim. - ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) - defer cancel() chanUpdates, tempPsbt, err := openChannelPsbt( ctxt, carol, dave, lntest.OpenChannelParams{ Amt: chanSize, @@ -547,8 +536,6 @@ func testPsbtChanFundingSingleStep(net *lntest.NetworkHarness, t *harnessTest) { err = decodedPsbt.Serialize(&psbtBytes) require.NoError(t.t, err) - ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout) - defer cancel() fundReq := &walletrpc.FundPsbtRequest{ Template: &walletrpc.FundPsbtRequest_Psbt{ Psbt: psbtBytes.Bytes(), @@ -583,8 +570,6 @@ func testPsbtChanFundingSingleStep(net *lntest.NetworkHarness, t *harnessTest) { // Now we'll ask Alice's wallet to sign the PSBT so we can finish the // funding flow. - ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout) - defer cancel() finalizeReq := &walletrpc.FinalizePsbtRequest{ FundedPsbt: fundResp.FundedPsbt, } @@ -604,8 +589,6 @@ func testPsbtChanFundingSingleStep(net *lntest.NetworkHarness, t *harnessTest) { // Consume the "channel pending" update. This waits until the funding // transaction was fully compiled. - ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout) - defer cancel() updateResp, err := receiveChanUpdate(ctxt, chanUpdates) require.NoError(t.t, err) upd, ok := updateResp.Update.(*lnrpc.OpenStatusUpdate_ChanPending) @@ -634,8 +617,6 @@ func testPsbtChanFundingSingleStep(net *lntest.NetworkHarness, t *harnessTest) { Memo: "new chans", Value: int64(payAmt), } - ctxt, cancel = context.WithTimeout(ctxb, defaultTimeout) - defer cancel() resp, err := dave.AddInvoice(ctxt, invoice) require.NoError(t.t, err) err = completePaymentRequests( @@ -651,6 +632,182 @@ func testPsbtChanFundingSingleStep(net *lntest.NetworkHarness, t *harnessTest) { closeChannelAndAssert(t, net, carol, chanPoint, false) } +// runSignPsbt tests that the SignPsbt RPC works correctly. +func testSignPsbt(net *lntest.NetworkHarness, t *harnessTest) { + runSignPsbt(t, net, net.Alice) +} + +// runSignPsbt tests that the SignPsbt RPC works correctly. +func runSignPsbt(t *harnessTest, net *lntest.NetworkHarness, + alice *lntest.HarnessNode) { + + ctxb := context.Background() + + // Everything we do here should be done within a second or two, so we + // can just keep a single timeout context around for all calls. + ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + + // We test that we can sign a PSBT that spends funds from an input that + // the wallet doesn't know about. To set up that test case, we first + // derive an address manually that the wallet won't be watching on + // chain. We can do that by exporting the account xpub of lnd's main + // account. + accounts, err := alice.WalletKitClient.ListAccounts( + ctxt, &walletrpc.ListAccountsRequest{}, + ) + require.NoError(t.t, err) + require.NotEmpty(t.t, accounts.Accounts) + + // We also need to parse the accounts, so we have easy access to the + // parsed derivation paths. + parsedAccounts, err := walletrpc.AccountsToWatchOnly(accounts.Accounts) + require.NoError(t.t, err) + + account := parsedAccounts[0] + xpub, err := hdkeychain.NewKeyFromString(account.Xpub) + require.NoError(t.t, err) + + const ( + changeIndex = 1 + addrIndex = 1337 + ) + fullDerivationPath := []uint32{ + hdkeychain.HardenedKeyStart + account.Purpose, + hdkeychain.HardenedKeyStart + account.CoinType, + hdkeychain.HardenedKeyStart + account.Account, + changeIndex, + addrIndex, + } + + // Let's simulate a change address. + change, err := xpub.DeriveNonStandard(changeIndex) // nolint:staticcheck + require.NoError(t.t, err) + + // At an index that we are certainly not watching in the wallet. + addrKey, err := change.DeriveNonStandard(addrIndex) // nolint:staticcheck + require.NoError(t.t, err) + + addrPubKey, err := addrKey.ECPubKey() + require.NoError(t.t, err) + pubKeyHash := btcutil.Hash160(addrPubKey.SerializeCompressed()) + witnessAddr, err := btcutil.NewAddressWitnessPubKeyHash( + pubKeyHash, harnessNetParams, + ) + require.NoError(t.t, err) + + pkScript, err := txscript.PayToAddrScript(witnessAddr) + require.NoError(t.t, err) + + // Let's send some coins to that address now. + utxo := &wire.TxOut{ + Value: 600_000, + PkScript: pkScript, + } + resp, err := alice.WalletKitClient.SendOutputs( + ctxt, &walletrpc.SendOutputsRequest{ + Outputs: []*signrpc.TxOut{{ + Value: utxo.Value, + PkScript: utxo.PkScript, + }}, + MinConfs: 0, + SpendUnconfirmed: true, + SatPerKw: 2500, + }, + ) + require.NoError(t.t, err) + + prevTx := wire.NewMsgTx(2) + err = prevTx.Deserialize(bytes.NewReader(resp.RawTx)) + require.NoError(t.t, err) + + prevOut := -1 + for idx, txOut := range prevTx.TxOut { + if bytes.Equal(txOut.PkScript, pkScript) { + prevOut = idx + } + } + require.Greater(t.t, prevOut, -1) + + // Okay, we have everything we need to create a PSBT now. + pendingTx := &wire.MsgTx{ + Version: 2, + TxIn: []*wire.TxIn{{ + PreviousOutPoint: wire.OutPoint{ + Hash: prevTx.TxHash(), + Index: uint32(prevOut), + }, + }}, + // We send to the same address again, but deduct some fees. + TxOut: []*wire.TxOut{{ + Value: utxo.Value - 600, + PkScript: utxo.PkScript, + }}, + } + packet, err := psbt.NewFromUnsignedTx(pendingTx) + require.NoError(t.t, err) + + // Now let's add the meta information that we need for signing. + packet.Inputs[0].Bip32Derivation = []*psbt.Bip32Derivation{{ + PubKey: addrPubKey.SerializeCompressed(), + Bip32Path: fullDerivationPath, + }} + packet.Inputs[0].WitnessUtxo = utxo + packet.Inputs[0].NonWitnessUtxo = prevTx + packet.Inputs[0].SighashType = txscript.SigHashAll + + // That's it, we should be able to sign the PSBT now. + var buf bytes.Buffer + err = packet.Serialize(&buf) + require.NoError(t.t, err) + + signResp, err := alice.WalletKitClient.SignPsbt( + ctxt, &walletrpc.SignPsbtRequest{ + FundedPsbt: buf.Bytes(), + }, + ) + require.NoError(t.t, err) + + // Let's make sure we have a partial signature. + signedPacket, err := psbt.NewFromRawBytes( + bytes.NewReader(signResp.SignedPsbt), false, + ) + require.NoError(t.t, err) + + require.Len(t.t, signedPacket.Inputs, 1) + require.Len(t.t, signedPacket.Inputs[0].PartialSigs, 1) + + partialSig := signedPacket.Inputs[0].PartialSigs[0] + require.Equal(t.t, partialSig.PubKey, addrPubKey.SerializeCompressed()) + require.Greater(t.t, len(partialSig.Signature), btcec.MinSigLen) + + // We should be able to finalize the PSBT and extract the final TX now. + err = psbt.MaybeFinalizeAll(signedPacket) + require.NoError(t.t, err) + + finalTx, err := psbt.Extract(signedPacket) + require.NoError(t.t, err) + + buf.Reset() + err = finalTx.Serialize(&buf) + require.NoError(t.t, err) + + // Publish the second transaction and then mine both of them. + _, err = alice.WalletKitClient.PublishTransaction( + ctxt, &walletrpc.Transaction{ + TxHex: buf.Bytes(), + }, + ) + require.NoError(t.t, err) + + // Mine one block which should contain two transactions. + block := mineBlocks(t, net, 1, 2)[0] + firstTxHash := prevTx.TxHash() + secondTxHash := finalTx.TxHash() + assertTxInBlock(t, block, &firstTxHash) + assertTxInBlock(t, block, &secondTxHash) +} + // openChannelPsbt attempts to open a channel between srcNode and destNode with // the passed channel funding parameters. If the passed context has a timeout, // then if the timeout is reached before the channel pending notification is diff --git a/lntest/itest/lnd_remote_signer_test.go b/lntest/itest/lnd_remote_signer_test.go index b1bae04cb..16c677949 100644 --- a/lntest/itest/lnd_remote_signer_test.go +++ b/lntest/itest/lnd_remote_signer_test.go @@ -166,6 +166,7 @@ func testRemoteSigner(net *lntest.NetworkHarness, t *harnessTest) { name: "psbt", fn: func(tt *harnessTest, wo, carol *lntest.HarnessNode) { runPsbtChanFunding(net, tt, carol, wo) + runSignPsbt(tt, net, wo) }, }} diff --git a/lntest/itest/lnd_test_list_on_test.go b/lntest/itest/lnd_test_list_on_test.go index 21a80fdc5..f2ee9d407 100644 --- a/lntest/itest/lnd_test_list_on_test.go +++ b/lntest/itest/lnd_test_list_on_test.go @@ -283,6 +283,10 @@ var allTestCases = []*testCase{ name: "psbt channel funding external", test: testPsbtChanFundingExternal, }, + { + name: "sign psbt", + test: testSignPsbt, + }, { name: "batch channel funding", test: testBatchChanFunding, From 3d353a9879481606fce9a85716499f9163ad230b Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Wed, 5 Jan 2022 11:04:26 +0100 Subject: [PATCH 10/17] config_builder+rpcwallet: simplify RPC signing With the remote signing instance now not needing to know anything about addresses or current derivation indices, we don't need to forward any such calls to that instance and can simplify the RPCKeyRing considerably. --- config_builder.go | 2 +- lntest/itest/lnd_remote_signer_test.go | 4 - lnwallet/rpcwallet/rpcwallet.go | 734 ++++++++++++------------- 3 files changed, 351 insertions(+), 389 deletions(-) diff --git a/config_builder.go b/config_builder.go index f1a3f1c40..b32077aaa 100644 --- a/config_builder.go +++ b/config_builder.go @@ -687,7 +687,7 @@ func (d *RPCSignerWalletImpl) BuildChainControl( rpcKeyRing, err := rpcwallet.NewRPCKeyRing( baseKeyRing, walletController, - d.DefaultWalletImpl.cfg.RemoteSigner, + d.DefaultWalletImpl.cfg.RemoteSigner, walletConfig.CoinType, rpcwallet.DefaultRPCTimeout, ) if err != nil { diff --git a/lntest/itest/lnd_remote_signer_test.go b/lntest/itest/lnd_remote_signer_test.go index 16c677949..3ec897547 100644 --- a/lntest/itest/lnd_remote_signer_test.go +++ b/lntest/itest/lnd_remote_signer_test.go @@ -238,10 +238,6 @@ func testRemoteSigner(net *lntest.NetworkHarness, t *harnessTest) { if subTest.sendCoins { net.SendCoins(t.t, btcutil.SatoshiPerBitcoin, watchOnly) - assertAccountBalance( - t.t, signer, "default", - btcutil.SatoshiPerBitcoin, 0, - ) assertAccountBalance( t.t, watchOnly, "default", btcutil.SatoshiPerBitcoin, 0, diff --git a/lnwallet/rpcwallet/rpcwallet.go b/lnwallet/rpcwallet/rpcwallet.go index b31881d15..4d39613db 100644 --- a/lnwallet/rpcwallet/rpcwallet.go +++ b/lnwallet/rpcwallet/rpcwallet.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "crypto/x509" - "encoding/binary" "errors" "fmt" "io/ioutil" @@ -16,16 +15,15 @@ import ( "github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil/hdkeychain" "github.com/btcsuite/btcutil/psbt" - "github.com/btcsuite/btcwallet/waddrmgr" - btcwallet "github.com/btcsuite/btcwallet/wallet" + basewallet "github.com/btcsuite/btcwallet/wallet" "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/lnwallet/btcwallet" "github.com/lightningnetwork/lnd/lnwallet/chainfee" - "github.com/lightningnetwork/lnd/lnwallet/chanfunding" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/macaroons" "google.golang.org/grpc" @@ -58,6 +56,8 @@ type RPCKeyRing struct { watchOnlyKeyRing keychain.SecretKeyRing + coinType uint32 + rpcTimeout time.Duration signerClient signrpc.SignerClient @@ -74,7 +74,7 @@ var _ lnwallet.WalletController = (*RPCKeyRing)(nil) // delegates any signing or ECDH operations to the remove signer through RPC. func NewRPCKeyRing(watchOnlyKeyRing keychain.SecretKeyRing, watchOnlyWalletController lnwallet.WalletController, - remoteSigner *lncfg.RemoteSigner, + remoteSigner *lncfg.RemoteSigner, coinType uint32, rpcTimeout time.Duration) (*RPCKeyRing, error) { rpcConn, err := connectRPC( @@ -89,6 +89,7 @@ func NewRPCKeyRing(watchOnlyKeyRing keychain.SecretKeyRing, return &RPCKeyRing{ WalletController: watchOnlyWalletController, watchOnlyKeyRing: watchOnlyKeyRing, + coinType: coinType, rpcTimeout: rpcTimeout, signerClient: signrpc.NewSignerClient(rpcConn), walletClient: walletrpc.NewWalletKitClient(rpcConn), @@ -105,189 +106,7 @@ func NewRPCKeyRing(watchOnlyKeyRing keychain.SecretKeyRing, 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 -} - -// LastUnusedAddress returns the last *unused* address known by the wallet. An -// address is unused if it hasn't received any payments. This can be useful in -// UIs in order to continually show the "freshest" address without having to -// worry about "address inflation" caused by continual refreshing. Similar to -// NewAddress it can derive a specified address type, and also optionally a -// change address. The account parameter must be non-empty as it determines -// which account the address should be generated from. -func (r *RPCKeyRing) LastUnusedAddress(lnwallet.AddressType, - string) (btcutil.Address, error) { - - // Because the underlying wallet will create a new address if the last - // derived address has been used in the meantime, we would need to proxy - // that call as well. But since that's deep within the btcwallet code, - // we cannot easily proxy it without more refactoring. Since this is an - // address type that is probably not widely used we can probably get - // away with not supporting it. - return nil, fmt.Errorf("unused address types are not supported when " + - "remote signing is enabled") -} - -// 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 + return r.WalletController.NewAddress(addrType, change, account) } // SendOutputs funds, signs, and broadcasts a Bitcoin transaction paying out to @@ -296,7 +115,9 @@ func (r *RPCKeyRing) ImportPublicKey(pubKey *btcec.PublicKey, // // NOTE: This method requires the global coin selection lock to be held. // -// This is a part of the WalletController interface. +// NOTE: This is a part of the WalletController interface. +// +// NOTE: This method only signs with BIP49/84 keys. func (r *RPCKeyRing) SendOutputs(outputs []*wire.TxOut, feeRate chainfee.SatPerKWeight, minConfs int32, label string) (*wire.MsgTx, error) { @@ -304,7 +125,7 @@ func (r *RPCKeyRing) SendOutputs(outputs []*wire.TxOut, tx, err := r.WalletController.SendOutputs( outputs, feeRate, minConfs, label, ) - if err != nil && err != btcwallet.ErrTxUnsigned { + if err != nil && err != basewallet.ErrTxUnsigned { return nil, err } if err == nil { @@ -324,7 +145,9 @@ func (r *RPCKeyRing) SendOutputs(outputs []*wire.TxOut, // watch-only wallet if it can map this outpoint into a coin we // own. If not, then we can't continue because our wallet state // is out of sync. - info, err := r.coinFromOutPoint(txIn.PreviousOutPoint) + info, err := r.WalletController.FetchInputInfo( + &txIn.PreviousOutPoint, + ) if err != nil { return nil, fmt.Errorf("error looking up utxo: %v", err) } @@ -332,7 +155,7 @@ func (r *RPCKeyRing) SendOutputs(outputs []*wire.TxOut, // Now that we know the input is ours, we'll populate the // signDesc with the per input unique information. signDesc.Output = &wire.TxOut{ - Value: info.Value, + Value: int64(info.Value), PkScript: info.PkScript, } signDesc.InputIndex = i @@ -351,19 +174,17 @@ func (r *RPCKeyRing) SendOutputs(outputs []*wire.TxOut, return tx, r.WalletController.PublishTransaction(tx, label) } -// FinalizePsbt expects a partial transaction with all inputs and outputs fully -// declared and tries to sign all inputs that belong to the specified account. -// Lnd must be the last signer of the transaction. That means, if there are any -// unsigned non-witness inputs or inputs without UTXO information attached or -// inputs without witness data that do not belong to lnd's wallet, this method -// will fail. If no error is returned, the PSBT is ready to be extracted and the -// final TX within to be broadcast. +// SignPsbt expects a partial transaction with all inputs and outputs fully +// declared and tries to sign all unsigned inputs that have all required fields +// (UTXO information, BIP32 derivation information, witness or sig scripts) set. +// If no error is returned, the PSBT is ready to be given to the next signer or +// to be finalized if lnd was the last signer. // -// NOTE: This method does NOT publish the transaction after it's been -// finalized successfully. -// -// This is a part of the WalletController interface. -func (r *RPCKeyRing) FinalizePsbt(packet *psbt.Packet, accountName string) error { +// NOTE: This RPC only signs inputs (and only those it can sign), it does not +// perform any other tasks (such as coin selection, UTXO locking or +// input/output/fee value validation, PSBT finalization). Any input that is +// incomplete will be skipped. +func (r *RPCKeyRing) SignPsbt(packet *psbt.Packet) error { ctxt, cancel := context.WithTimeout(context.Background(), r.rpcTimeout) defer cancel() @@ -372,14 +193,11 @@ func (r *RPCKeyRing) FinalizePsbt(packet *psbt.Packet, accountName string) error return fmt.Errorf("error serializing PSBT: %v", err) } - resp, err := r.walletClient.FinalizePsbt( - ctxt, &walletrpc.FinalizePsbtRequest{ - FundedPsbt: buf.Bytes(), - Account: accountName, - }, - ) + resp, err := r.walletClient.SignPsbt(ctxt, &walletrpc.SignPsbtRequest{ + FundedPsbt: buf.Bytes(), + }) if err != nil { - return fmt.Errorf("error finalizing PSBT in remote signer "+ + return fmt.Errorf("error signing PSBT in remote signer "+ "instance: %v", err) } @@ -401,6 +219,137 @@ func (r *RPCKeyRing) FinalizePsbt(packet *psbt.Packet, accountName string) error return nil } +// FinalizePsbt expects a partial transaction with all inputs and outputs fully +// declared and tries to sign all inputs that belong to the specified account. +// Lnd must be the last signer of the transaction. That means, if there are any +// unsigned non-witness inputs or inputs without UTXO information attached or +// inputs without witness data that do not belong to lnd's wallet, this method +// will fail. If no error is returned, the PSBT is ready to be extracted and the +// final TX within to be broadcast. +// +// NOTE: This method does NOT publish the transaction after it's been +// finalized successfully. +// +// NOTE: This is a part of the WalletController interface. +// +// NOTE: We need to overwrite this method because we need to redirect the call +// to ComputeInputScript to the RPC key ring's implementation. If we forward +// the call to the default WalletController implementation, we get an error +// since that wallet is watch-only. If we forward the call to the remote signer, +// we get an error because the signer doesn't know the UTXO information required +// in ComputeInputScript. +// +// TODO(guggero): Refactor btcwallet to accept ComputeInputScript as a function +// parameter in FinalizePsbt so we can get rid of this code duplication. +func (r *RPCKeyRing) FinalizePsbt(packet *psbt.Packet, _ string) error { + // Let's check that this is actually something we can and want to sign. + // We need at least one input and one output. + err := psbt.VerifyInputOutputLen(packet, true, true) + if err != nil { + return err + } + + // Go through each input that doesn't have final witness data attached + // to it already and try to sign it. We do expect that we're the last + // ones to sign. If there is any input without witness data that we + // cannot sign because it's not our UTXO, this will be a hard failure. + tx := packet.UnsignedTx + sigHashes := txscript.NewTxSigHashes(tx) + for idx, txIn := range tx.TxIn { + in := packet.Inputs[idx] + + // We can only sign if we have UTXO information available. We + // can just continue here as a later step will fail with a more + // precise error message. + if in.WitnessUtxo == nil && in.NonWitnessUtxo == nil { + continue + } + + // Skip this input if it's got final witness data attached. + if len(in.FinalScriptWitness) > 0 { + continue + } + + // We can only sign this input if it's ours, so we try to map it + // to a coin we own. If we can't, then we'll continue as it + // isn't our input. + utxo, err := r.FetchInputInfo(&txIn.PreviousOutPoint) + if err != nil { + continue + } + fullTx := utxo.PrevTx + signDesc := &input.SignDescriptor{ + KeyDesc: keychain.KeyDescriptor{}, + Output: &wire.TxOut{ + Value: int64(utxo.Value), + PkScript: utxo.PkScript, + }, + HashType: in.SighashType, + SigHashes: sigHashes, + InputIndex: idx, + } + + // Find out what UTXO we are signing. Wallets _should_ always + // provide the full non-witness UTXO for segwit v0. + var signOutput *wire.TxOut + if in.NonWitnessUtxo != nil { + prevIndex := txIn.PreviousOutPoint.Index + signOutput = in.NonWitnessUtxo.TxOut[prevIndex] + + if !psbt.TxOutsEqual(signDesc.Output, signOutput) { + return fmt.Errorf("found UTXO %#v but it "+ + "doesn't match PSBT's input %v", + signDesc.Output, signOutput) + } + + if fullTx.TxHash() != txIn.PreviousOutPoint.Hash { + return fmt.Errorf("found UTXO tx %v but it "+ + "doesn't match PSBT's input %v", + fullTx.TxHash(), + txIn.PreviousOutPoint.Hash) + } + } + + // Fall back to witness UTXO only for older wallets. + if in.WitnessUtxo != nil { + signOutput = in.WitnessUtxo + + if !psbt.TxOutsEqual(signDesc.Output, signOutput) { + return fmt.Errorf("found UTXO %#v but it "+ + "doesn't match PSBT's input %v", + signDesc.Output, signOutput) + } + } + + // Do the actual signing in ComputeInputScript which in turn + // will invoke the remote signer. + script, err := r.ComputeInputScript(tx, signDesc) + if err != nil { + return fmt.Errorf("error computing input script for "+ + "input %d: %v", idx, err) + } + + // Serialize the witness format from the stack representation to + // the wire representation. + var witnessBytes bytes.Buffer + err = psbt.WriteTxWitness(&witnessBytes, script.Witness) + if err != nil { + return fmt.Errorf("error serializing witness: %v", err) + } + packet.Inputs[idx].FinalScriptWitness = witnessBytes.Bytes() + packet.Inputs[idx].FinalScriptSig = script.SigScript + } + + // Make sure the PSBT itself thinks it's finalized and ready to be + // broadcast. + err = psbt.MaybeFinalizeAll(packet) + if err != nil { + return fmt.Errorf("error finalizing PSBT: %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. @@ -409,40 +358,7 @@ func (r *RPCKeyRing) FinalizePsbt(packet *psbt.Packet, accountName string) error func (r *RPCKeyRing) DeriveNextKey( keyFam keychain.KeyFamily) (keychain.KeyDescriptor, error) { - ctxt, cancel := context.WithTimeout(context.Background(), r.rpcTimeout) - defer cancel() - - // We need to keep the local and remote wallet in sync. That's why we - // first attempt to also derive the next key on the remote wallet. - remoteDesc, err := r.walletClient.DeriveNextKey(ctxt, &walletrpc.KeyReq{ - KeyFamily: int32(keyFam), - }) - if err != nil { - return keychain.KeyDescriptor{}, fmt.Errorf("error deriving "+ - "key on remote signer instance: %v", err) - } - - localDesc, err := r.watchOnlyKeyRing.DeriveNextKey(keyFam) - if err != nil { - return keychain.KeyDescriptor{}, fmt.Errorf("error deriving "+ - "key on local wallet instance: %v", err) - } - - // We never know if the administrator of the remote signing wallet does - // manual calls to next address or whatever. So we cannot be certain - // that we're always fully in sync. But as long as our local index is - // lower or equal to the remote index we know the remote wallet should - // 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 { - return keychain.KeyDescriptor{}, fmt.Errorf("error deriving "+ - "key on remote signer instance, derived index %d was "+ - "lower than local index %d", remoteDesc.KeyLoc.KeyIndex, - localDesc.Index) - } - - return localDesc, nil + return r.watchOnlyKeyRing.DeriveNextKey(keyFam) } // DeriveKey attempts to derive an arbitrary key specified by the passed @@ -453,41 +369,7 @@ func (r *RPCKeyRing) DeriveNextKey( func (r *RPCKeyRing) DeriveKey( keyLoc keychain.KeyLocator) (keychain.KeyDescriptor, error) { - ctxt, cancel := context.WithTimeout(context.Background(), r.rpcTimeout) - defer cancel() - - // We need to keep the local and remote wallet in sync. That's why we - // first attempt to also derive the same key on the remote wallet. - remoteDesc, err := r.walletClient.DeriveKey(ctxt, &signrpc.KeyLocator{ - KeyFamily: int32(keyLoc.Family), - KeyIndex: int32(keyLoc.Index), - }) - if err != nil { - return keychain.KeyDescriptor{}, fmt.Errorf("error deriving "+ - "key on remote signer instance: %v", err) - } - - localDesc, err := r.watchOnlyKeyRing.DeriveKey(keyLoc) - if err != nil { - return keychain.KeyDescriptor{}, fmt.Errorf("error deriving "+ - "key on local wallet instance: %v", err) - } - - // We never know if the administrator of the remote signing wallet does - // manual calls to next address or whatever. So we cannot be certain - // that we're always fully in sync. But as long as our local index is - // lower or equal to the remote index we know the remote wallet should - // 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 { - return keychain.KeyDescriptor{}, fmt.Errorf("error deriving "+ - "key on remote signer instance, derived index %d was "+ - "lower than local index %d", remoteDesc.KeyLoc.KeyIndex, - localDesc.Index) - } - - return localDesc, nil + return r.watchOnlyKeyRing.DeriveKey(keyLoc) } // ECDH performs a scalar multiplication (ECDH-like operation) between the @@ -618,23 +500,15 @@ func (r *RPCKeyRing) DerivePrivKey(_ keychain.KeyDescriptor) (*btcec.PrivateKey, // NOTE: The resulting signature should be void of a sighash byte. // // NOTE: This method is part of the input.Signer interface. +// +// NOTE: This method only signs with BIP1017 (internal) keys! func (r *RPCKeyRing) SignOutputRaw(tx *wire.MsgTx, signDesc *input.SignDescriptor) (input.Signature, error) { - ctxt, cancel := context.WithTimeout(context.Background(), r.rpcTimeout) - defer cancel() - - rpcSignReq, err := toRPCSignReq(tx, signDesc) - if err != nil { - return nil, err - } - - resp, err := r.signerClient.SignOutputRaw(ctxt, rpcSignReq) - if err != nil { - return nil, err - } - - return btcec.ParseDERSignature(resp.RawSigs[0], btcec.S256()) + // Forward the call to the remote signing instance. This call is only + // ever called for signing witness (p2pkh or p2wsh) inputs and never + // nested witness inputs, so the sigScript is always nil. + return r.remoteSign(tx, signDesc, nil) } // ComputeInputScript generates a complete InputIndex for the passed @@ -651,109 +525,162 @@ func (r *RPCKeyRing) SignOutputRaw(tx *wire.MsgTx, func (r *RPCKeyRing) ComputeInputScript(tx *wire.MsgTx, signDesc *input.SignDescriptor) (*input.Script, error) { + addr, witnessProgram, sigScript, err := r.WalletController.ScriptForOutput( + signDesc.Output, + ) + if err != nil { + return nil, err + } + signDesc.WitnessScript = witnessProgram + + // Let's give the TX to the remote instance now, so it can sign the + // input. + sig, err := r.remoteSign(tx, signDesc, sigScript) + if err != nil { + return nil, fmt.Errorf("error signing with remote instance: %v", + err) + } + + // ComputeInputScript currently is only used for P2WKH and NP2WKH + // addresses. So the last item on the stack is always the compressed + // public key. + return &input.Script{ + Witness: wire.TxWitness{ + append(sig.Serialize(), byte(signDesc.HashType)), + addr.PubKey().SerializeCompressed(), + }, + SigScript: sigScript, + }, nil +} + +// remoteSign signs the input specified in signDesc of the given transaction tx +// using the remote signing instance. +func (r *RPCKeyRing) remoteSign(tx *wire.MsgTx, signDesc *input.SignDescriptor, + sigScript []byte) (input.Signature, error) { + + packet, err := packetFromTx(tx) + if err != nil { + return nil, fmt.Errorf("error converting TX into PSBT: %v", err) + } + + // Catch incorrect signing input index, just in case. + if signDesc.InputIndex < 0 || signDesc.InputIndex >= len(packet.Inputs) { + return nil, fmt.Errorf("invalid input index in sign descriptor") + } + in := &packet.Inputs[signDesc.InputIndex] + txIn := tx.TxIn[signDesc.InputIndex] + + // Make sure we actually know about the input. We either have been + // watching the UTXO on-chain or we have been given all the required + // info in the sign descriptor. + info, err := r.WalletController.FetchInputInfo(&txIn.PreviousOutPoint) + switch { + // No error, we do have the full UTXO and derivation info available. + case err == nil: + in.WitnessUtxo = &wire.TxOut{ + Value: int64(info.Value), + PkScript: info.PkScript, + } + in.NonWitnessUtxo = info.PrevTx + in.Bip32Derivation = []*psbt.Bip32Derivation{info.Derivation} + + // The wallet doesn't know about this UTXO, so it's probably a TX that + // we haven't published yet (e.g. a channel funding TX). So we need to + // assemble everything from the sign descriptor. We won't be able to + // supply a non-witness UTXO (=full TX of the input being spent) in this + // case. That is no problem if the signing instance is another lnd + // instance since we don't require it for pure witness inputs. But a + // hardware wallet might require it for security reasons. + case signDesc.KeyDesc.PubKey != nil && signDesc.Output != nil: + in.WitnessUtxo = signDesc.Output + in.Bip32Derivation = []*psbt.Bip32Derivation{{ + Bip32Path: []uint32{ + keychain.BIP0043Purpose + + hdkeychain.HardenedKeyStart, + r.coinType + hdkeychain.HardenedKeyStart, + uint32(signDesc.KeyDesc.Family) + + hdkeychain.HardenedKeyStart, + 0, + signDesc.KeyDesc.Index, + }, + PubKey: signDesc.KeyDesc.PubKey.SerializeCompressed(), + }} + + default: + return nil, fmt.Errorf("error assembling UTXO information, "+ + "wallet returned err='%v' and sign descriptor is "+ + "incomplete", err) + } + + // Assemble all other information about the input we have. + in.RedeemScript = sigScript + in.SighashType = signDesc.HashType + in.WitnessScript = signDesc.WitnessScript + + if len(signDesc.SingleTweak) > 0 { + in.Unknowns = append(in.Unknowns, &psbt.Unknown{ + Key: btcwallet.PsbtKeyTypeInputSignatureTweakSingle, + Value: signDesc.SingleTweak, + }) + } + if signDesc.DoubleTweak != nil { + in.Unknowns = append(in.Unknowns, &psbt.Unknown{ + Key: btcwallet.PsbtKeyTypeInputSignatureTweakDouble, + Value: signDesc.DoubleTweak.Serialize(), + }) + } + + // Okay, let's sign the input by the remote signer now. ctxt, cancel := context.WithTimeout(context.Background(), r.rpcTimeout) defer cancel() - rpcSignReq, err := toRPCSignReq(tx, signDesc) - if err != nil { - return nil, err - } - - resp, err := r.signerClient.ComputeInputScript(ctxt, rpcSignReq) - if err != nil { - return nil, err - } - - return &input.Script{ - Witness: resp.InputScripts[0].Witness, - SigScript: resp.InputScripts[0].SigScript, - }, nil -} - -// coinFromOutPoint attempts to locate details pertaining to a coin based on -// its outpoint. If the coin isn't under the control of the backing watch-only -// wallet, then an error is returned. -func (r *RPCKeyRing) coinFromOutPoint(op wire.OutPoint) (*chanfunding.Coin, - error) { - - inputInfo, err := r.WalletController.FetchInputInfo(&op) - if err != nil { - return nil, err - } - - return &chanfunding.Coin{ - TxOut: wire.TxOut{ - Value: int64(inputInfo.Value), - PkScript: inputInfo.PkScript, - }, - OutPoint: inputInfo.OutPoint, - }, nil -} - -// toRPCSignReq converts the given raw transaction and sign descriptors into -// their corresponding RPC counterparts. -func toRPCSignReq(tx *wire.MsgTx, - signDesc *input.SignDescriptor) (*signrpc.SignReq, error) { - - if signDesc.Output == nil { - return nil, fmt.Errorf("need output to sign") - } - var buf bytes.Buffer - if err := tx.Serialize(&buf); err != nil { - return nil, err + if err := packet.Serialize(&buf); err != nil { + return nil, fmt.Errorf("error serializing PSBT: %v", err) } - rpcSignDesc := &signrpc.SignDescriptor{ - KeyDesc: &signrpc.KeyDescriptor{ - KeyLoc: &signrpc.KeyLocator{ - KeyFamily: int32(signDesc.KeyDesc.Family), - KeyIndex: int32(signDesc.KeyDesc.Index), - }, - }, - SingleTweak: signDesc.SingleTweak, - WitnessScript: signDesc.WitnessScript, - Output: &signrpc.TxOut{ - Value: signDesc.Output.Value, - PkScript: signDesc.Output.PkScript, - }, - Sighash: uint32(signDesc.HashType), - InputIndex: int32(signDesc.InputIndex), + resp, err := r.walletClient.SignPsbt( + ctxt, &walletrpc.SignPsbtRequest{FundedPsbt: buf.Bytes()}, + ) + if err != nil { + return nil, fmt.Errorf("error signing PSBT in remote signer "+ + "instance: %v", err) } - if signDesc.KeyDesc.PubKey != nil { - rpcSignDesc.KeyDesc.RawKeyBytes = - signDesc.KeyDesc.PubKey.SerializeCompressed() - } - if signDesc.DoubleTweak != nil { - rpcSignDesc.DoubleTweak = signDesc.DoubleTweak.Serialize() + signedPacket, err := psbt.NewFromRawBytes( + bytes.NewReader(resp.SignedPsbt), false, + ) + if err != nil { + return nil, fmt.Errorf("error parsing signed PSBT: %v", err) } - return &signrpc.SignReq{ - RawTxBytes: buf.Bytes(), - SignDescs: []*signrpc.SignDescriptor{rpcSignDesc}, - }, 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 + // We expect a signature in the input now. + if signDesc.InputIndex >= len(signedPacket.Inputs) { + return nil, fmt.Errorf("remote signer returned invalid PSBT") + } + in = &signedPacket.Inputs[signDesc.InputIndex] + if len(in.PartialSigs) != 1 { + return nil, fmt.Errorf("remote signer returned invalid "+ + "partial signature, wanted 1, got %d", + len(in.PartialSigs)) + } + sigWithSigHash := in.PartialSigs[0] + if sigWithSigHash == nil { + return nil, fmt.Errorf("remote signer returned nil signature") } - 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) + // The remote signer always adds the sighash type, so we need to account + // for that. + if len(sigWithSigHash.Signature) < btcec.MinSigLen+1 { + return nil, fmt.Errorf("remote signer returned invalid "+ + "partial signature: signature too short with %d bytes", + len(sigWithSigHash.Signature)) } + + // Parse the signature, but chop off the last byte which is the sighash + // type. + sig := sigWithSigHash.Signature[0 : len(sigWithSigHash.Signature)-1] + return btcec.ParseDERSignature(sig, btcec.S256()) } // connectRPC tries to establish an RPC connection to the given host:port with @@ -802,3 +729,42 @@ func connectRPC(hostPort, tlsCertPath, macaroonPath string) (*grpc.ClientConn, return conn, nil } + +// packetFromTx creates a PSBT from a tx that potentially already contains +// signed inputs. +func packetFromTx(original *wire.MsgTx) (*psbt.Packet, error) { + // The psbt.NewFromUnsignedTx function complains if there are any + // scripts or witness content on a TX. So we create a copy of the TX and + // nil out all the offending data, but also keep a backup around that we + // add to the PSBT afterwards. + noSigs := original.Copy() + for idx := range noSigs.TxIn { + noSigs.TxIn[idx].SignatureScript = nil + noSigs.TxIn[idx].Witness = nil + } + + // With all the data that is seen as "signed", we can now create the + // empty packet. + packet, err := psbt.NewFromUnsignedTx(noSigs) + if err != nil { + return nil, err + } + + var buf bytes.Buffer + for idx, txIn := range original.TxIn { + if len(txIn.SignatureScript) > 0 { + packet.Inputs[idx].FinalScriptSig = txIn.SignatureScript + } + + if len(txIn.Witness) > 0 { + buf.Reset() + err = psbt.WriteTxWitness(&buf, txIn.Witness) + if err != nil { + return nil, err + } + packet.Inputs[idx].FinalScriptWitness = buf.Bytes() + } + } + + return packet, nil +} From f3cd383d1f4964d01537fc1ff53a1129ebcbec41 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Wed, 5 Jan 2022 11:04:27 +0100 Subject: [PATCH 11/17] rpcwallet: add critical log to remote signer errors --- lnwallet/rpcwallet/rpcwallet.go | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/lnwallet/rpcwallet/rpcwallet.go b/lnwallet/rpcwallet/rpcwallet.go index 4d39613db..dc1894fd5 100644 --- a/lnwallet/rpcwallet/rpcwallet.go +++ b/lnwallet/rpcwallet/rpcwallet.go @@ -197,8 +197,12 @@ func (r *RPCKeyRing) SignPsbt(packet *psbt.Packet) error { FundedPsbt: buf.Bytes(), }) if err != nil { - return fmt.Errorf("error signing PSBT in remote signer "+ + err = fmt.Errorf("error signing PSBT in remote signer "+ "instance: %v", err) + + // Log as critical as we should shut down if there is no signer. + log.Criticalf("RPC signer error: %v", err) + return err } signedPacket, err := psbt.NewFromRawBytes( @@ -404,6 +408,11 @@ func (r *RPCKeyRing) ECDH(keyDesc keychain.KeyDescriptor, resp, err := r.signerClient.DeriveSharedKey(ctxt, req) if err != nil { + err = fmt.Errorf("error deriving shared key in remote signer "+ + "instance: %v", err) + + // Log as critical as we should shut down if there is no signer. + log.Criticalf("RPC signer error: %v", err) return key, err } @@ -432,6 +441,11 @@ func (r *RPCKeyRing) SignMessage(keyLoc keychain.KeyLocator, DoubleHash: doubleHash, }) if err != nil { + err = fmt.Errorf("error signing message in remote signer "+ + "instance: %v", err) + + // Log as critical as we should shut down if there is no signer. + log.Criticalf("RPC signer error: %v", err) return nil, err } @@ -468,6 +482,11 @@ func (r *RPCKeyRing) SignMessageCompact(keyLoc keychain.KeyLocator, CompactSig: true, }) if err != nil { + err = fmt.Errorf("error signing message in remote signer "+ + "instance: %v", err) + + // Log as critical as we should shut down if there is no signer. + log.Criticalf("RPC signer error: %v", err) return nil, err } @@ -643,8 +662,12 @@ func (r *RPCKeyRing) remoteSign(tx *wire.MsgTx, signDesc *input.SignDescriptor, ctxt, &walletrpc.SignPsbtRequest{FundedPsbt: buf.Bytes()}, ) if err != nil { - return nil, fmt.Errorf("error signing PSBT in remote signer "+ + err = fmt.Errorf("error signing PSBT in remote signer "+ "instance: %v", err) + + // Log as critical as we should shut down if there is no signer. + log.Criticalf("RPC signer error: %v", err) + return nil, err } signedPacket, err := psbt.NewFromRawBytes( From 9601a9ab848cb03b5e4a3cf1e582dba834d6372c Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Wed, 5 Jan 2022 11:04:28 +0100 Subject: [PATCH 12/17] multi: make remote signer RPC timeout configurable --- config.go | 5 ++++- config_builder.go | 1 - lncfg/remotesigner.go | 35 +++++++++++++++++++++++++++++---- lnwallet/rpcwallet/rpcwallet.go | 22 +++++++++------------ sample-lnd.conf | 3 +++ 5 files changed, 47 insertions(+), 19 deletions(-) diff --git a/config.go b/config.go index 6666942ce..65b3b75c0 100644 --- a/config.go +++ b/config.go @@ -579,7 +579,9 @@ func DefaultConfig() Config { ChannelCommitInterval: defaultChannelCommitInterval, ChannelCommitBatchSize: defaultChannelCommitBatchSize, CoinSelectionStrategy: defaultCoinSelectionStrategy, - RemoteSigner: &lncfg.RemoteSigner{}, + RemoteSigner: &lncfg.RemoteSigner{ + Timeout: lncfg.DefaultRemoteSignerRPCTimeout, + }, } } @@ -1554,6 +1556,7 @@ func ValidateConfig(cfg Config, interceptor signal.Interceptor, fileParser, cfg.Cluster, cfg.HealthChecks, cfg.RPCMiddleware, + cfg.RemoteSigner, ) if err != nil { return nil, err diff --git a/config_builder.go b/config_builder.go index b32077aaa..1fd9fb116 100644 --- a/config_builder.go +++ b/config_builder.go @@ -688,7 +688,6 @@ func (d *RPCSignerWalletImpl) BuildChainControl( rpcKeyRing, err := rpcwallet.NewRPCKeyRing( baseKeyRing, walletController, d.DefaultWalletImpl.cfg.RemoteSigner, walletConfig.CoinType, - rpcwallet.DefaultRPCTimeout, ) if err != nil { err := fmt.Errorf("unable to create RPC remote signing wallet "+ diff --git a/lncfg/remotesigner.go b/lncfg/remotesigner.go index 90d1988cf..c4a0821d6 100644 --- a/lncfg/remotesigner.go +++ b/lncfg/remotesigner.go @@ -1,9 +1,36 @@ package lncfg +import ( + "fmt" + "time" +) + +const ( + // DefaultRemoteSignerRPCTimeout is the default timeout that is used + // when forwarding a request to the remote signer through RPC. + DefaultRemoteSignerRPCTimeout = 5 * time.Second +) + // RemoteSigner holds the configuration options for a remote RPC signer. type RemoteSigner struct { - Enable bool `long:"enable" description:"Use a remote signer for signing any on-chain related transactions or messages. Only recommended if local wallet is initialized as watch-only. Remote signer must use the same seed/root key as the local watch-only wallet but must have private keys."` - RPCHost string `long:"rpchost" description:"The remote signer's RPC host:port"` - MacaroonPath string `long:"macaroonpath" description:"The macaroon to use for authenticating with the remote signer"` - TLSCertPath string `long:"tlscertpath" description:"The TLS certificate to use for establishing the remote signer's identity"` + Enable bool `long:"enable" description:"Use a remote signer for signing any on-chain related transactions or messages. Only recommended if local wallet is initialized as watch-only. Remote signer must use the same seed/root key as the local watch-only wallet but must have private keys."` + RPCHost string `long:"rpchost" description:"The remote signer's RPC host:port"` + MacaroonPath string `long:"macaroonpath" description:"The macaroon to use for authenticating with the remote signer"` + TLSCertPath string `long:"tlscertpath" description:"The TLS certificate to use for establishing the remote signer's identity"` + Timeout time.Duration `long:"timeout" description:"The timeout for connecting to and signing requests with the remote signer. Valid time units are {s, m, h}."` +} + +// Validate checks the values configured for our remote RPC signer. +func (r *RemoteSigner) Validate() error { + if !r.Enable { + return nil + } + + if r.Timeout < time.Millisecond { + return fmt.Errorf("remote signer: timeout of %v is invalid, "+ + "cannot be smaller than %v", r.Timeout, + time.Millisecond) + } + + return nil } diff --git a/lnwallet/rpcwallet/rpcwallet.go b/lnwallet/rpcwallet/rpcwallet.go index dc1894fd5..01f9f5449 100644 --- a/lnwallet/rpcwallet/rpcwallet.go +++ b/lnwallet/rpcwallet/rpcwallet.go @@ -31,12 +31,6 @@ import ( "gopkg.in/macaroon.v2" ) -const ( - // DefaultRPCTimeout is the default timeout that is used when forwarding - // a request to the remote signer through RPC. - DefaultRPCTimeout = 5 * time.Second -) - var ( // ErrRemoteSigningPrivateKeyNotAvailable is the error that is returned // if an operation is requested from the RPC wallet that is not @@ -74,12 +68,11 @@ var _ lnwallet.WalletController = (*RPCKeyRing)(nil) // delegates any signing or ECDH operations to the remove signer through RPC. func NewRPCKeyRing(watchOnlyKeyRing keychain.SecretKeyRing, watchOnlyWalletController lnwallet.WalletController, - remoteSigner *lncfg.RemoteSigner, coinType uint32, - rpcTimeout time.Duration) (*RPCKeyRing, error) { + remoteSigner *lncfg.RemoteSigner, coinType uint32) (*RPCKeyRing, error) { rpcConn, err := connectRPC( remoteSigner.RPCHost, remoteSigner.TLSCertPath, - remoteSigner.MacaroonPath, + remoteSigner.MacaroonPath, remoteSigner.Timeout, ) if err != nil { return nil, fmt.Errorf("error connecting to the remote "+ @@ -90,7 +83,7 @@ func NewRPCKeyRing(watchOnlyKeyRing keychain.SecretKeyRing, WalletController: watchOnlyWalletController, watchOnlyKeyRing: watchOnlyKeyRing, coinType: coinType, - rpcTimeout: rpcTimeout, + rpcTimeout: remoteSigner.Timeout, signerClient: signrpc.NewSignerClient(rpcConn), walletClient: walletrpc.NewWalletKitClient(rpcConn), }, nil @@ -708,8 +701,8 @@ func (r *RPCKeyRing) remoteSign(tx *wire.MsgTx, signDesc *input.SignDescriptor, // 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, - error) { +func connectRPC(hostPort, tlsCertPath, macaroonPath string, + timeout time.Duration) (*grpc.ClientConn, error) { certBytes, err := ioutil.ReadFile(tlsCertPath) if err != nil { @@ -743,8 +736,11 @@ func connectRPC(hostPort, tlsCertPath, macaroonPath string) (*grpc.ClientConn, cp, "", )), grpc.WithPerRPCCredentials(macCred), + grpc.WithBlock(), } - conn, err := grpc.Dial(hostPort, opts...) + ctxt, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + conn, err := grpc.DialContext(ctxt, hostPort, opts...) if err != nil { return nil, fmt.Errorf("unable to connect to RPC server: %v", err) diff --git a/sample-lnd.conf b/sample-lnd.conf index 36b1e4bb0..bccc2f3a1 100644 --- a/sample-lnd.conf +++ b/sample-lnd.conf @@ -1253,6 +1253,9 @@ litecoin.node=ltcd ; The TLS certificate to use for establishing the remote signer's identity. ; remotesigner.tlscertpath=/path/to/remote/signer/tls.cert +; The timeout for connecting to and signing requests with the remote signer. +; Valid time units are {s, m, h}. +; remotesigner.timeout=5s [gossip] From da59c1fa6207de6b4bd169233902fff13046b551 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Wed, 5 Jan 2022 11:04:30 +0100 Subject: [PATCH 13/17] multi: add health check for remote signer --- config.go | 16 ++++++++++++++++ lncfg/healthcheck.go | 2 ++ lnwallet/rpcwallet/healthcheck.go | 24 ++++++++++++++++++++++++ sample-lnd.conf | 17 +++++++++++++++++ server.go | 27 +++++++++++++++++++++++++++ 5 files changed, 86 insertions(+) create mode 100644 lnwallet/rpcwallet/healthcheck.go diff --git a/config.go b/config.go index 65b3b75c0..0928ead3e 100644 --- a/config.go +++ b/config.go @@ -148,6 +148,16 @@ const ( defaultTCBackoff = time.Minute defaultTCAttempts = 0 + // Set defaults for a health check which ensures that the remote signer + // RPC connection is alive. Although this check is off by default (only + // active when remote signing is turned on), we still set the other + // default values so that the health check can be easily enabled with + // sane defaults. + defaultRSInterval = time.Minute + defaultRSTimeout = time.Second * 1 + defaultRSBackoff = time.Second * 30 + defaultRSAttempts = 1 + // defaultRemoteMaxHtlcs specifies the default limit for maximum // concurrent HTLCs the remote party may add to commitment transactions. // This value can be overridden with --default-remote-max-htlcs. @@ -558,6 +568,12 @@ func DefaultConfig() Config { Attempts: defaultTCAttempts, Backoff: defaultTCBackoff, }, + RemoteSigner: &lncfg.CheckConfig{ + Interval: defaultRSInterval, + Timeout: defaultRSTimeout, + Attempts: defaultRSAttempts, + Backoff: defaultRSBackoff, + }, }, Gossip: &lncfg.Gossip{ MaxChannelUpdateBurst: discovery.DefaultMaxChannelUpdateBurst, diff --git a/lncfg/healthcheck.go b/lncfg/healthcheck.go index 2c2a77c99..ce668ea08 100644 --- a/lncfg/healthcheck.go +++ b/lncfg/healthcheck.go @@ -30,6 +30,8 @@ type HealthCheckConfig struct { TLSCheck *CheckConfig `group:"tls" namespace:"tls"` TorConnection *CheckConfig `group:"torconnection" namespace:"torconnection"` + + RemoteSigner *CheckConfig `group:"remotesigner" namespace:"remotesigner"` } // Validate checks the values configured for our health checks. diff --git a/lnwallet/rpcwallet/healthcheck.go b/lnwallet/rpcwallet/healthcheck.go new file mode 100644 index 000000000..27bcb9c73 --- /dev/null +++ b/lnwallet/rpcwallet/healthcheck.go @@ -0,0 +1,24 @@ +package rpcwallet + +import ( + "fmt" + "time" + + "github.com/lightningnetwork/lnd/lncfg" +) + +// HealthCheck returns a health check function for the given remote signing +// configuration. +func HealthCheck(cfg *lncfg.RemoteSigner, timeout time.Duration) func() error { + return func() error { + _, err := connectRPC( + cfg.RPCHost, cfg.TLSCertPath, cfg.MacaroonPath, timeout, + ) + if err != nil { + return fmt.Errorf("error connecting to the remote "+ + "signing node through RPC: %v", err) + } + + return nil + } +} diff --git a/sample-lnd.conf b/sample-lnd.conf index bccc2f3a1..f9a81167e 100644 --- a/sample-lnd.conf +++ b/sample-lnd.conf @@ -1006,6 +1006,23 @@ litecoin.node=ltcd ; value must be >= 1m. ; healthcheck.torconnection.interval=1m +; The number of times we should attempt to check our remote signer RPC +; connection before gracefully shutting down. Set this value to 0 to disable +; this health check. +; healthcheck.remotesigner.attempts=1 + +; The amount of time we allow a call to our remote signer RPC connection to take +; before we fail the attempt. This value must be >= 1s. +; healthcheck.remotesigner.timeout=1s + +; The amount of time we should backoff between failed attempts to check remote +; signer RPC connection. This value must be >= 1s. +; healthcheck.remotesigner.backoff=30s + +; The amount of time we should wait between remote signer RPC connection health +; checks. This value must be >= 1m. +; healthcheck.remotesigner.interval=1m + [signrpc] diff --git a/server.go b/server.go index f387ea701..d86e27a4f 100644 --- a/server.go +++ b/server.go @@ -52,6 +52,7 @@ import ( "github.com/lightningnetwork/lnd/lnrpc/routerrpc" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet/chainfee" + "github.com/lightningnetwork/lnd/lnwallet/rpcwallet" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/nat" "github.com/lightningnetwork/lnd/netann" @@ -1584,6 +1585,32 @@ func (s *server) createLivenessMonitor(cfg *Config, cc *chainreg.ChainControl) { checks = append(checks, torConnectionCheck) } + // If remote signing is enabled, add the healthcheck for the remote + // signing RPC interface. + if s.cfg.RemoteSigner != nil && s.cfg.RemoteSigner.Enable { + // Because we have two cascading timeouts here, we need to add + // some slack to the "outer" one of them in case the "inner" + // returns exactly on time. + overhead := time.Millisecond * 10 + + remoteSignerConnectionCheck := healthcheck.NewObservation( + "remote signer connection", + rpcwallet.HealthCheck( + s.cfg.RemoteSigner, + + // For the health check we might to be even + // stricter than the initial/normal connect, so + // we use the health check timeout here. + cfg.HealthChecks.RemoteSigner.Timeout, + ), + cfg.HealthChecks.RemoteSigner.Interval, + cfg.HealthChecks.RemoteSigner.Timeout+overhead, + cfg.HealthChecks.RemoteSigner.Backoff, + cfg.HealthChecks.RemoteSigner.Attempts, + ) + checks = append(checks, remoteSignerConnectionCheck) + } + // If we have not disabled all of our health checks, we create a // liveliness monitor with our configured checks. s.livelinessMonitor = healthcheck.NewMonitor( From 0bdac59a8cd617cfccb3b2c2aada335a640a5163 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Wed, 5 Jan 2022 11:04:31 +0100 Subject: [PATCH 14/17] multi: add nochainbackend option --- chainreg/chainregistry.go | 16 +++ chainreg/no_chain_backend.go | 215 +++++++++++++++++++++++++++++++++++ config.go | 4 + lncfg/chain.go | 2 +- 4 files changed, 236 insertions(+), 1 deletion(-) create mode 100644 chainreg/no_chain_backend.go diff --git a/chainreg/chainregistry.go b/chainreg/chainregistry.go index 8364b67df..48cc5df37 100644 --- a/chainreg/chainregistry.go +++ b/chainreg/chainregistry.go @@ -690,6 +690,22 @@ func NewPartialChainControl(cfg *Config) (*PartialChainControl, func(), error) { return nil, nil, err } } + + case "nochainbackend": + backend := &NoChainBackend{} + source := &NoChainSource{ + BestBlockTime: time.Now(), + } + + cc.ChainNotifier = backend + cc.ChainView = backend + cc.FeeEstimator = backend + + cc.ChainSource = source + cc.HealthCheck = func() error { + return nil + } + default: return nil, nil, fmt.Errorf("unknown node type: %s", homeChainConfig.Node) diff --git a/chainreg/no_chain_backend.go b/chainreg/no_chain_backend.go new file mode 100644 index 000000000..8f5bc0fbe --- /dev/null +++ b/chainreg/no_chain_backend.go @@ -0,0 +1,215 @@ +package chainreg + +import ( + "errors" + "time" + + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcwallet/chain" + "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/lightningnetwork/lnd/chainntnfs" + "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/lnwallet/chainfee" + "github.com/lightningnetwork/lnd/routing/chainview" +) + +var ( + // defaultFee is the fee that is returned by NoChainBackend. + defaultFee = chainfee.FeePerKwFloor + + // noChainBackendName is the backend name returned by NoChainBackend. + noChainBackendName = "nochainbackend" + + // errNotImplemented is the error that is returned by NoChainBackend for + // any operation that is not supported by it. Such paths should in + // practice never been hit, so seeing this error either means a remote + // signing instance was used for an unsupported purpose or a previously + // forgotten edge case path was hit. + errNotImplemented = errors.New("not implemented in nochainbackend " + + "mode") + + // noChainBackendBestHash is the chain hash of the chain tip that is + // returned by NoChainBackend. + noChainBackendBestHash = &chainhash.Hash{0x01} + + // noChainBackendBestHeight is the best height that is returned by + // NoChainBackend. + noChainBackendBestHeight int32 = 1 +) + +// NoChainBackend is a mock implementation of the following interfaces: +// - chainview.FilteredChainView +// - chainntnfs.ChainNotifier +// - chainfee.Estimator +type NoChainBackend struct { +} + +func (n *NoChainBackend) EstimateFeePerKW(uint32) (chainfee.SatPerKWeight, + error) { + + return defaultFee, nil +} + +func (n *NoChainBackend) RelayFeePerKW() chainfee.SatPerKWeight { + return defaultFee +} + +func (n *NoChainBackend) RegisterConfirmationsNtfn(*chainhash.Hash, []byte, + uint32, uint32) (*chainntnfs.ConfirmationEvent, error) { + + return nil, errNotImplemented +} + +func (n *NoChainBackend) RegisterSpendNtfn(*wire.OutPoint, []byte, + uint32) (*chainntnfs.SpendEvent, error) { + + return nil, errNotImplemented +} + +func (n *NoChainBackend) RegisterBlockEpochNtfn( + *chainntnfs.BlockEpoch) (*chainntnfs.BlockEpochEvent, error) { + + epochChan := make(chan *chainntnfs.BlockEpoch) + return &chainntnfs.BlockEpochEvent{ + Epochs: epochChan, + Cancel: func() { + close(epochChan) + }, + }, nil +} + +func (n *NoChainBackend) Started() bool { + return true +} + +func (n *NoChainBackend) FilteredBlocks() <-chan *chainview.FilteredBlock { + return make(chan *chainview.FilteredBlock) +} + +func (n *NoChainBackend) DisconnectedBlocks() <-chan *chainview.FilteredBlock { + return make(chan *chainview.FilteredBlock) +} + +func (n *NoChainBackend) UpdateFilter([]channeldb.EdgePoint, uint32) error { + return nil +} + +func (n *NoChainBackend) FilterBlock(*chainhash.Hash) (*chainview.FilteredBlock, + error) { + + return nil, errNotImplemented +} + +func (n *NoChainBackend) Start() error { + return nil +} + +func (n *NoChainBackend) Stop() error { + return nil +} + +var _ chainview.FilteredChainView = (*NoChainBackend)(nil) +var _ chainntnfs.ChainNotifier = (*NoChainBackend)(nil) +var _ chainfee.Estimator = (*NoChainBackend)(nil) + +// NoChainSource is a mock implementation of chain.Interface. +// The mock is designed to return static values where necessary to make any +// caller believe the chain is fully synced to virtual block height 1 (hash +// 0x0000..0001). That should avoid calls to other methods completely since they +// are only used for advancing the chain forward. +type NoChainSource struct { + notifChan chan interface{} + + BestBlockTime time.Time +} + +func (n *NoChainSource) Start() error { + n.notifChan = make(chan interface{}) + + go func() { + n.notifChan <- &chain.RescanFinished{ + Hash: noChainBackendBestHash, + Height: noChainBackendBestHeight, + Time: n.BestBlockTime, + } + }() + + return nil +} + +func (n *NoChainSource) Stop() { +} + +func (n *NoChainSource) WaitForShutdown() { +} + +func (n *NoChainSource) GetBestBlock() (*chainhash.Hash, int32, error) { + return noChainBackendBestHash, noChainBackendBestHeight, nil +} + +func (n *NoChainSource) GetBlock(*chainhash.Hash) (*wire.MsgBlock, error) { + return &wire.MsgBlock{ + Header: wire.BlockHeader{ + Timestamp: n.BestBlockTime, + }, + Transactions: []*wire.MsgTx{}, + }, nil +} + +func (n *NoChainSource) GetBlockHash(int64) (*chainhash.Hash, error) { + return noChainBackendBestHash, nil +} + +func (n *NoChainSource) GetBlockHeader(*chainhash.Hash) (*wire.BlockHeader, + error) { + + return &wire.BlockHeader{ + Timestamp: n.BestBlockTime, + }, nil +} + +func (n *NoChainSource) IsCurrent() bool { + return true +} + +func (n *NoChainSource) FilterBlocks( + *chain.FilterBlocksRequest) (*chain.FilterBlocksResponse, error) { + + return nil, errNotImplemented +} + +func (n *NoChainSource) BlockStamp() (*waddrmgr.BlockStamp, error) { + return nil, errNotImplemented +} + +func (n *NoChainSource) SendRawTransaction(*wire.MsgTx, bool) (*chainhash.Hash, + error) { + + return nil, errNotImplemented +} + +func (n *NoChainSource) Rescan(*chainhash.Hash, []btcutil.Address, + map[wire.OutPoint]btcutil.Address) error { + + return nil +} + +func (n *NoChainSource) NotifyReceived([]btcutil.Address) error { + return nil +} + +func (n *NoChainSource) NotifyBlocks() error { + return nil +} + +func (n *NoChainSource) Notifications() <-chan interface{} { + return n.notifChan +} + +func (n *NoChainSource) BackEnd() string { + return noChainBackendName +} + +var _ chain.Interface = (*NoChainSource)(nil) diff --git a/config.go b/config.go index 0928ead3e..218b42b24 100644 --- a/config.go +++ b/config.go @@ -1230,6 +1230,10 @@ func ValidateConfig(cfg Config, interceptor signal.Interceptor, fileParser, case "neutrino": // No need to get RPC parameters. + case "nochainbackend": + // Nothing to configure, we're running without any chain + // backend whatsoever (pure signing mode). + default: str := "only btcd, bitcoind, and neutrino mode " + "supported for bitcoin at this time" diff --git a/lncfg/chain.go b/lncfg/chain.go index 014cd19ca..0ee4e3ada 100644 --- a/lncfg/chain.go +++ b/lncfg/chain.go @@ -11,7 +11,7 @@ type Chain struct { Active bool `long:"active" description:"If the chain should be active or not."` ChainDir string `long:"chaindir" description:"The directory to store the chain's data within."` - Node string `long:"node" description:"The blockchain interface to use." choice:"btcd" choice:"bitcoind" choice:"neutrino" choice:"ltcd" choice:"litecoind"` + Node string `long:"node" description:"The blockchain interface to use." choice:"btcd" choice:"bitcoind" choice:"neutrino" choice:"ltcd" choice:"litecoind" choice:"nochainbackend"` MainNet bool `long:"mainnet" description:"Use the main network"` TestNet3 bool `long:"testnet" description:"Use the test network"` From afc53d1c52f457ba24dc2a82d0f78e87865113b5 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Wed, 5 Jan 2022 11:04:32 +0100 Subject: [PATCH 15/17] lnwallet: initialize first 255 accounts This fixes lightninglabs/loop#437 by adding all accounts that are used in liquidity products such as Loop or Pool. Since both of these products use key families below 255, we can get by with that number. The alternative to creating way too many accounts (which increases the default wallet size by ~250kB) would be to hard code the exact accounts used by Loop (99) and Pool (210). But that sounds like a bad idea given that there could always be more accounts being added to those (or other) products. By making sure the first 255 accounts exist, we have a lot more flexibility in those products for choosing key families. --- lntest/itest/lnd_remote_signer_test.go | 125 +++++++++++-------------- lnwallet/btcwallet/btcwallet.go | 16 ++-- 2 files changed, 60 insertions(+), 81 deletions(-) diff --git a/lntest/itest/lnd_remote_signer_test.go b/lntest/itest/lnd_remote_signer_test.go index 3ec897547..4a4162c6f 100644 --- a/lntest/itest/lnd_remote_signer_test.go +++ b/lntest/itest/lnd_remote_signer_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcutil/hdkeychain" "github.com/btcsuite/btcwallet/waddrmgr" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lnrpc" @@ -39,76 +40,6 @@ var ( Xpub: "tpubDDWAWrSLRSFrG1KdqXMQQyTKYGSKLKaY7gxpvK7RdV3e3Dkhvu" + "W2GgsFvsPN4RGmuoYtUgZ1LHZE8oftz7T4mzc1BxGt5rt8zJcVQi" + "KTPPV", - }, { - Purpose: keychain.BIP0043Purpose, - CoinType: harnessNetParams.HDCoinType, - Account: uint32(keychain.KeyFamilyMultiSig), - Xpub: "tpubDDXFHr67Ro2tHKVWG2gNjjijKUH1Lyv5NKFYdJnuaLGVNBVwyV" + - "5AbykhR43iy8wYozEMbw2QfmAqZhb8gnuL5mm9sZh8YsR6FjGAbe" + - "w1xoT", - }, { - Purpose: keychain.BIP0043Purpose, - CoinType: harnessNetParams.HDCoinType, - Account: uint32(keychain.KeyFamilyRevocationBase), - Xpub: "tpubDDXFHr67Ro2tKkccDqNfDqZpd5wCs2n6XRV2Uh185DzCTbkDaE" + - "d9v7P837zZTYBNVfaRriuxgGVgxbGjDui4CKxyzBzwz4aAZxjn2P" + - "hNcQy", - }, { - Purpose: keychain.BIP0043Purpose, - CoinType: harnessNetParams.HDCoinType, - Account: uint32(keychain.KeyFamilyHtlcBase), - Xpub: "tpubDDXFHr67Ro2tNH4KH41i4oTsWfRjFigoH1Ee7urvHow51opH9x" + - "J7mu1qSPMPVtkVqQZ5tE4NTuFJPrbDqno7TQietyUDmPTwyVviJb" + - "GCwXk", - }, { - Purpose: keychain.BIP0043Purpose, - CoinType: harnessNetParams.HDCoinType, - Account: uint32(keychain.KeyFamilyPaymentBase), - Xpub: "tpubDDXFHr67Ro2tQj5Zvav2ALhkU6dRQAhEtNPnYJVBC8hs2U1A9e" + - "cqxRY3XTiJKBDD7e8tudhmTRs8aGWJAiAXJN5kXy3Hi6cmiwGWjX" + - "K5Cv5", - }, { - Purpose: keychain.BIP0043Purpose, - CoinType: harnessNetParams.HDCoinType, - Account: uint32(keychain.KeyFamilyDelayBase), - Xpub: "tpubDDXFHr67Ro2tSSR2LLBJtotxx2U45cuESLWKA72YT9td3SzVKH" + - "AptzDEx5chsUNZ4WRMY5h6HJxRSebjRatxQKX1uUsux1LvKS1wsf" + - "NJ2PH", - }, { - Purpose: keychain.BIP0043Purpose, - CoinType: harnessNetParams.HDCoinType, - Account: uint32(keychain.KeyFamilyRevocationRoot), - Xpub: "tpubDDXFHr67Ro2tTwzfWvNoMoPpZbxdMEfe1WhbXJxvXikGixPa4g" + - "gSRZeGx6T5yxVHTVT3rjVh35Veqsowj7emX8SZfXKDKDKcLduXCe" + - "WPUU3", - }, { - Purpose: keychain.BIP0043Purpose, - CoinType: harnessNetParams.HDCoinType, - Account: uint32(keychain.KeyFamilyNodeKey), - Xpub: "tpubDDXFHr67Ro2tYEDS2EByRedfsUoEwBtrzVbS1qdPrX6sAkUYGL" + - "rZWvMmQv8KZDZ4zd9r8WzM9bJ2nGp7XuNVC4w2EBtWg7i76gbrmu" + - "EWjQh", - }, { - Purpose: keychain.BIP0043Purpose, - CoinType: harnessNetParams.HDCoinType, - Account: uint32(keychain.KeyFamilyStaticBackup), - Xpub: "tpubDDXFHr67Ro2tYpwnFJEQaM8eAPM2UV5uY6gFgXeSzS5aC5T9Tf" + - "zXuawYKBbQMZJn8qHXLafY4tAutoda1aKP5h6Nbgy3swPbnhWbFj" + - "S5wnX", - }, { - Purpose: keychain.BIP0043Purpose, - CoinType: harnessNetParams.HDCoinType, - Account: uint32(keychain.KeyFamilyTowerSession), - Xpub: "tpubDDXFHr67Ro2tddKpAjUegXqt7EGxRXnHkeLbUkfuFMGbLJYgRp" + - "G4ew5pMmGg2nmcGmHFQ29w3juNhd8N5ZZ8HwJdymC4f5ukQLJ4yg" + - "9rEr3", - }, { - Purpose: keychain.BIP0043Purpose, - CoinType: harnessNetParams.HDCoinType, - Account: uint32(keychain.KeyFamilyTowerID), - Xpub: "tpubDDXFHr67Ro2tgE89V8ZdgMytC2Jq1iT9ttGhdzR1X7haQJNBmX" + - "t8kau6taC6DGASYzbrjmo9z9w6JQFcaLNqbhS2h2PVSzKf79j265" + - "Zi8hF", }} ) @@ -181,7 +112,7 @@ func testRemoteSigner(net *lntest.NetworkHarness, t *harnessTest) { password := []byte("itestpassword") var ( signerNodePubKey = nodePubKey - watchOnlyAccounts = accounts + watchOnlyAccounts = deriveCustomScopeAccounts(t.t) signer *lntest.HarnessNode err error ) @@ -261,3 +192,55 @@ func testRemoteSigner(net *lntest.NetworkHarness, t *harnessTest) { } } } + +// deriveCustomScopeAccounts derives the first 255 default accounts of the custom lnd +// internal key scope. +func deriveCustomScopeAccounts(t *testing.T) []*lnrpc.WatchOnlyAccount { + allAccounts := make([]*lnrpc.WatchOnlyAccount, 0, 255+len(accounts)) + allAccounts = append(allAccounts, accounts...) + + extendedRootKey, err := hdkeychain.NewKeyFromString(rootKey) + require.NoError(t, err) + + path := []uint32{ + keychain.BIP0043Purpose + hdkeychain.HardenedKeyStart, + harnessNetParams.HDCoinType + hdkeychain.HardenedKeyStart, + } + coinTypeKey, err := derivePath(extendedRootKey, path) + require.NoError(t, err) + for idx := uint32(0); idx <= 255; idx++ { + accountPath := []uint32{idx + hdkeychain.HardenedKeyStart} + accountKey, err := derivePath(coinTypeKey, accountPath) + require.NoError(t, err) + + accountXPub, err := accountKey.Neuter() + require.NoError(t, err) + + allAccounts = append(allAccounts, &lnrpc.WatchOnlyAccount{ + Purpose: keychain.BIP0043Purpose, + CoinType: harnessNetParams.HDCoinType, + Account: idx, + Xpub: accountXPub.String(), + }) + } + + return allAccounts +} + +// derivePath derives the given path from an extended key. +func derivePath(key *hdkeychain.ExtendedKey, path []uint32) ( + *hdkeychain.ExtendedKey, error) { + + var ( + currentKey = key + err error + ) + for _, pathPart := range path { + currentKey, err = currentKey.Derive(pathPart) + if err != nil { + return nil, err + } + } + + return currentKey, nil +} diff --git a/lnwallet/btcwallet/btcwallet.go b/lnwallet/btcwallet/btcwallet.go index bc4922eb6..579ca130d 100644 --- a/lnwallet/btcwallet/btcwallet.go +++ b/lnwallet/btcwallet/btcwallet.go @@ -323,17 +323,13 @@ func (b *BtcWallet) Start() error { err = walletdb.Update(b.db, func(tx walletdb.ReadWriteTx) error { addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) - for _, keyFam := range keychain.VersionZeroKeyFamilies { - // If this is the multi-sig key family, then we can - // return early as this is the default account that's - // created. - if keyFam == keychain.KeyFamilyMultiSig { - continue - } - + // Generate all accounts that we could ever need. This includes + // all lnd key families as well as some key families used in + // external liquidity tools. + for keyFam := uint32(1); keyFam <= 255; keyFam++ { // Otherwise, we'll check if the account already exists, // if so, we can once again bail early. - _, err := scope.AccountName(addrmgrNs, uint32(keyFam)) + _, err := scope.AccountName(addrmgrNs, keyFam) if err == nil { continue } @@ -341,7 +337,7 @@ func (b *BtcWallet) Start() error { // If we reach this point, then the account hasn't yet // been created, so we'll need to create it before we // can proceed. - err = scope.NewRawAccount(addrmgrNs, uint32(keyFam)) + err = scope.NewRawAccount(addrmgrNs, keyFam) if err != nil { return err } From bab807a57d51f055119f7161bc1b7a30e1376a10 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Wed, 5 Jan 2022 11:04:34 +0100 Subject: [PATCH 16/17] multi: add migrate-wallet-to-watch-only flag To enable converting an existing wallet with private key material into a watch-only wallet on first startup with remote signing enabled, we add a new flag. Since the conversion is a destructive process, this shouldn't happen automatically just because remote signing is enabled. --- config.go | 5 +++- config_builder.go | 40 +++++++++++++++++-------------- lncfg/remotesigner.go | 17 +++++++++---- lnwallet/btcwallet/btcwallet.go | 42 ++++++++++++++++++++++++++++++++- lnwallet/btcwallet/config.go | 7 ++++++ sample-lnd.conf | 7 ++++++ 6 files changed, 93 insertions(+), 25 deletions(-) diff --git a/config.go b/config.go index 218b42b24..1a59cd2e5 100644 --- a/config.go +++ b/config.go @@ -1612,7 +1612,10 @@ func (c *Config) ImplementationConfig( // watch-only source of chain and address data. But we don't need any // private key material in that btcwallet base wallet. if c.RemoteSigner.Enable { - rpcImpl := NewRPCSignerWalletImpl(c, ltndLog, interceptor) + rpcImpl := NewRPCSignerWalletImpl( + c, ltndLog, interceptor, + c.RemoteSigner.MigrateWatchOnly, + ) return &ImplementationCfg{ GrpcRegistrar: rpcImpl, RestRegistrar: rpcImpl, diff --git a/config_builder.go b/config_builder.go index 1fd9fb116..ed1b57711 100644 --- a/config_builder.go +++ b/config_builder.go @@ -144,8 +144,9 @@ type DefaultWalletImpl struct { logger btclog.Logger interceptor signal.Interceptor - watchOnly bool - pwService *walletunlocker.UnlockerService + watchOnly bool + migrateWatchOnly bool + pwService *walletunlocker.UnlockerService } // NewDefaultWalletImpl creates a new default wallet implementation. @@ -560,16 +561,17 @@ func (d *DefaultWalletImpl) BuildWalletConfig(ctx context.Context, } walletConfig := &btcwallet.Config{ - PrivatePass: privateWalletPw, - PublicPass: publicWalletPw, - Birthday: walletInitParams.Birthday, - RecoveryWindow: walletInitParams.RecoveryWindow, - NetParams: d.cfg.ActiveNetParams.Params, - CoinType: d.cfg.ActiveNetParams.CoinType, - Wallet: walletInitParams.Wallet, - LoaderOptions: []btcwallet.LoaderOption{dbs.WalletDB}, - ChainSource: partialChainControl.ChainSource, - WatchOnly: d.watchOnly, + PrivatePass: privateWalletPw, + PublicPass: publicWalletPw, + Birthday: walletInitParams.Birthday, + RecoveryWindow: walletInitParams.RecoveryWindow, + NetParams: d.cfg.ActiveNetParams.Params, + CoinType: d.cfg.ActiveNetParams.CoinType, + Wallet: walletInitParams.Wallet, + LoaderOptions: []btcwallet.LoaderOption{dbs.WalletDB}, + ChainSource: partialChainControl.ChainSource, + WatchOnly: d.watchOnly, + MigrateWatchOnly: d.migrateWatchOnly, } // Parse coin selection strategy. @@ -650,15 +652,17 @@ type RPCSignerWalletImpl struct { // NewRPCSignerWalletImpl creates a new instance of the remote signing wallet // implementation. func NewRPCSignerWalletImpl(cfg *Config, logger btclog.Logger, - interceptor signal.Interceptor) *RPCSignerWalletImpl { + interceptor signal.Interceptor, + migrateWatchOnly bool) *RPCSignerWalletImpl { return &RPCSignerWalletImpl{ DefaultWalletImpl: &DefaultWalletImpl{ - cfg: cfg, - logger: logger, - interceptor: interceptor, - watchOnly: true, - pwService: createWalletUnlockerService(cfg), + cfg: cfg, + logger: logger, + interceptor: interceptor, + watchOnly: true, + migrateWatchOnly: migrateWatchOnly, + pwService: createWalletUnlockerService(cfg), }, } } diff --git a/lncfg/remotesigner.go b/lncfg/remotesigner.go index c4a0821d6..66995122c 100644 --- a/lncfg/remotesigner.go +++ b/lncfg/remotesigner.go @@ -13,11 +13,12 @@ const ( // RemoteSigner holds the configuration options for a remote RPC signer. type RemoteSigner struct { - Enable bool `long:"enable" description:"Use a remote signer for signing any on-chain related transactions or messages. Only recommended if local wallet is initialized as watch-only. Remote signer must use the same seed/root key as the local watch-only wallet but must have private keys."` - RPCHost string `long:"rpchost" description:"The remote signer's RPC host:port"` - MacaroonPath string `long:"macaroonpath" description:"The macaroon to use for authenticating with the remote signer"` - TLSCertPath string `long:"tlscertpath" description:"The TLS certificate to use for establishing the remote signer's identity"` - Timeout time.Duration `long:"timeout" description:"The timeout for connecting to and signing requests with the remote signer. Valid time units are {s, m, h}."` + Enable bool `long:"enable" description:"Use a remote signer for signing any on-chain related transactions or messages. Only recommended if local wallet is initialized as watch-only. Remote signer must use the same seed/root key as the local watch-only wallet but must have private keys."` + RPCHost string `long:"rpchost" description:"The remote signer's RPC host:port"` + MacaroonPath string `long:"macaroonpath" description:"The macaroon to use for authenticating with the remote signer"` + TLSCertPath string `long:"tlscertpath" description:"The TLS certificate to use for establishing the remote signer's identity"` + Timeout time.Duration `long:"timeout" description:"The timeout for connecting to and signing requests with the remote signer. Valid time units are {s, m, h}."` + MigrateWatchOnly bool `long:"migrate-wallet-to-watch-only" description:"If a wallet with private key material already exists, migrate it into a watch-only wallet on first startup. WARNING: This cannot be undone! Make sure you have backed up your seed before you use this flag! All private keys will be purged from the wallet after first unlock with this flag!"` } // Validate checks the values configured for our remote RPC signer. @@ -32,5 +33,11 @@ func (r *RemoteSigner) Validate() error { time.Millisecond) } + if r.MigrateWatchOnly && !r.Enable { + return fmt.Errorf("remote signer: cannot turn on wallet " + + "migration to watch-only if remote signing is not " + + "enabled") + } + return nil } diff --git a/lnwallet/btcwallet/btcwallet.go b/lnwallet/btcwallet/btcwallet.go index 579ca130d..ddaf1f06c 100644 --- a/lnwallet/btcwallet/btcwallet.go +++ b/lnwallet/btcwallet/btcwallet.go @@ -287,15 +287,38 @@ func (b *BtcWallet) InternalWallet() *base.Wallet { // // This is a part of the WalletController interface. func (b *BtcWallet) Start() error { + // Is the wallet (according to its database) currently watch-only + // already? If it is, we won't need to convert it later. + walletIsWatchOnly := b.wallet.Manager.WatchOnly() + + // If the wallet is watch-only, but we don't expect it to be, then we + // are in an unexpected state and cannot continue. + if walletIsWatchOnly && !b.cfg.WatchOnly { + return fmt.Errorf("wallet is watch-only but we expect it " + + "not to be; check if remote signing was disabled by " + + "accident") + } + // We'll start by unlocking the wallet and ensuring that the KeyScope: // (1017, 1) exists within the internal waddrmgr. We'll need this in // order to properly generate the keys required for signing various // contracts. If this is a watch-only wallet, we don't have any private // keys and therefore unlocking is not necessary. - if !b.cfg.WatchOnly { + if !walletIsWatchOnly { if err := b.wallet.Unlock(b.cfg.PrivatePass, nil); err != nil { return err } + + // If the wallet isn't about to be converted, we need to inform + // the user that this wallet still contains all private key + // material and that they need to migrate the existing wallet. + if b.cfg.WatchOnly && !b.cfg.MigrateWatchOnly { + log.Warnf("Wallet is expected to be in watch-only " + + "mode but hasn't been migrated to watch-only " + + "yet, it still contains private keys; " + + "consider turning on the watch-only wallet " + + "migration in remote signing mode") + } } scope, err := b.wallet.Manager.FetchScopedKeyManager(b.chainKeyScope) @@ -343,6 +366,23 @@ func (b *BtcWallet) Start() error { } } + // If this is the first startup with remote signing and wallet + // migration turned on and the wallet wasn't previously + // migrated, we can do that now that we made sure all accounts + // that we need were derived correctly. + if !walletIsWatchOnly && b.cfg.WatchOnly && + b.cfg.MigrateWatchOnly { + + log.Infof("Migrating wallet to watch-only mode, " + + "purging all private key material") + + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + err = b.wallet.Manager.ConvertToWatchingOnly(ns) + if err != nil { + return err + } + } + return nil }) if err != nil { diff --git a/lnwallet/btcwallet/config.go b/lnwallet/btcwallet/config.go index f97f2943f..b9c642c5d 100644 --- a/lnwallet/btcwallet/config.go +++ b/lnwallet/btcwallet/config.go @@ -76,6 +76,13 @@ type Config struct { // WatchOnly indicates that the wallet was initialized with public key // material only and does not contain any private keys. WatchOnly bool + + // MigrateWatchOnly indicates that if a wallet with private key material + // already exists, it should be attempted to be converted into a + // watch-only wallet on first startup. This flag has no effect if no + // wallet exists and a watch-only one is created directly, or, if the + // wallet was previously converted to a watch-only already. + MigrateWatchOnly bool } // NetworkDir returns the directory name of a network directory to hold wallet diff --git a/sample-lnd.conf b/sample-lnd.conf index f9a81167e..295d64c60 100644 --- a/sample-lnd.conf +++ b/sample-lnd.conf @@ -1274,6 +1274,13 @@ litecoin.node=ltcd ; Valid time units are {s, m, h}. ; remotesigner.timeout=5s +; If a wallet with private key material already exists, migrate it into a +; watch-only wallet on first startup. +; WARNING: This cannot be undone! Make sure you have backed up your seed before +; you use this flag! All private keys will be purged from the wallet after first +; unlock with this flag! +; remotesigner.migrate-wallet-to-watch-only=true + [gossip] ; Specify a set of pinned gossip syncers, which will always be actively syncing From 5282c9acf97e98d792ab77291566b9599f2accc6 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Wed, 5 Jan 2022 11:04:35 +0100 Subject: [PATCH 17/17] docs: update remote signing doc and release notes --- docs/release-notes/release-notes-0.14.2.md | 21 +++++-- docs/remote-signing.md | 72 +++++++++++----------- 2 files changed, 53 insertions(+), 40 deletions(-) diff --git a/docs/release-notes/release-notes-0.14.2.md b/docs/release-notes/release-notes-0.14.2.md index 775e5edcc..bf2a9f777 100644 --- a/docs/release-notes/release-notes-0.14.2.md +++ b/docs/release-notes/release-notes-0.14.2.md @@ -1,12 +1,22 @@ # Release Notes +## Remote signing + +The [remote signing](../remote-signing.md) setup was simplified in that the +signing node now [does not need to be hooked up to its own chain +backend](https://github.com/lightningnetwork/lnd/pull/6006). A new mock chain +backend can be specified with `--bitcoin.node=nochainbackend`. That way a wallet +will be created and all signing RPCs work but the node will not look at any +chain data. It can therefore be fully offline except for a single incoming gRPC +connection from the watch-only node. + ## Wallet * A bug that prevented opening anchor-based channels from external wallets when the internal wallet was empty even though the transaction contained a sufficiently large output belonging to the internal wallet - [was fixed](https://github.com/lightningnetwork/lnd/pull/5539) - In other words, freshly-installed LND can now be initailized with multiple + [was fixed](https://github.com/lightningnetwork/lnd/pull/5539). + In other words, freshly-installed LND can now be initialized with multiple channels from an external (e.g. hardware) wallet *in a single transaction*. ## Build System @@ -29,10 +39,11 @@ * [We now _always_ set a channel type if the other party signals the feature bit](https://github.com/lightningnetwork/lnd/pull/6075). -* [Add json flag to - trackpayment](https://github.com/lightningnetwork/lnd/pull/6060) +* [Add `--json` flag to + `trackpayment`](https://github.com/lightningnetwork/lnd/pull/6060). + * [Clarify invalid config timeout - constraints](https://github.com/lightningnetwork/lnd/pull/6073) + constraints](https://github.com/lightningnetwork/lnd/pull/6073). * [Fix memory corruption in Mission Control Store](https://github.com/lightningnetwork/lnd/pull/6068) diff --git a/docs/remote-signing.md b/docs/remote-signing.md index 9e4561526..6bcef1b57 100644 --- a/docs/remote-signing.md +++ b/docs/remote-signing.md @@ -9,7 +9,7 @@ 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 and the outbound connection to `bitcoind` (or `btcd` or `neutrino`). +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. @@ -31,40 +31,14 @@ xxx xx v | | +----------------+ gRPC | +----------------+ | | watch-only lnd +--------------+-->| full seed lnd | | - +-------+--------+ | +------+---------+ | - | | | | - +-------v--------+ | +------v---------+ | - | bitcoind/btcd | | | bitcoind/btcd | | - +----------------+ | +----------------+ | - | | - +----------------------------------+ + +-------+--------+ | +----------------+ | + | | | + +-------v--------+ +----------------------------------+ + | bitcoind/btcd | + +----------------+ + ``` -NOTE: Offline in this context means that `lnd` itself does not need to be -reachable from the internet and itself doesn't connect out. If the `bitcoind` -or other chain backend is indeed running within this same firewall/offline zone -then that component will need at least _outbound_ internet access. But it is -also possible to use a chain backend that is running outside this zone. - -## Restrictions / limitations - -The current implementation of the remote signing mode comes with a few -restrictions and limitations: - -- Both `lnd` instances need a connection to a chain backend: - - The type of chain backend (`bitcoind`, `btcd` or `neutrino`) **must** - match. - - Both instances can point to the same chain backend node, though limits - apply with the number of `lnd` instances that can use the same `bitcoind` - node over ZMQ. - See ["running multiple lnd nodes" in the safety guide](safety.md#running-multiple-lnd-nodes) - . - - Using a pruned chain backend is not recommended as that increases the - chance of the two wallets falling out of sync with each other. -- The wallet of the "signer" instance **must not be used** for anything. - Especially generating new addresses manually on the "signer" will lead to the - two wallets falling out of sync! - ## Example setup In this example we are going to set up two nodes, the "signer" that has the full @@ -80,8 +54,8 @@ opened to this node from the host on which the node "watch-only" is running. Recommended entries in `lnd.conf`: ```text -# No special configuration required other than basic "hardening" parameters to -# make sure no connections to the internet are opened. +# 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. @@ -93,6 +67,19 @@ 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 + +# 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 ``` After successfully starting up "signer", the following command can be run to @@ -151,6 +138,21 @@ 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. +## 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. + ## Example initialization script This section shows an example script that initializes the watch-only wallet of