Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

waddrmgr+wallet: support transaction creation and signing for watch-only accounts #733

Merged
merged 10 commits into from
Mar 29, 2021
Merged

waddrmgr+wallet: support transaction creation and signing for watch-only accounts #733

merged 10 commits into from
Mar 29, 2021

Conversation

wpaulino
Copy link
Contributor

This PR aims to address the missing gaps for watch-only accounts to actually be used in the context of transaction creation and signing. It also includes some minor changes and surfaces some additional account information that will serve useful for higher level applications and their use of watch-only accounts.

Depends on #732.

@wpaulino
Copy link
Contributor Author

cc @Roasbeef @guggero

Copy link
Member

@Roasbeef Roasbeef left a comment

Choose a reason for hiding this comment

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

One step closer! The diff reads well, but I'm having a hard time wrapping my head around exactly what an account uint32 means in various areas of the codebase now. Perhaps we should introduce a new term like accountID that refers to the auto incrementing account number within a given scoped key manager?

wallet/utxos.go Outdated Show resolved Hide resolved
wallet/utxos.go Show resolved Hide resolved
wallet/psbt.go Outdated Show resolved Hide resolved
rpc/legacyrpc/methods.go Show resolved Hide resolved
wallet/createtx.go Show resolved Hide resolved
wallet/psbt.go Show resolved Hide resolved
Copy link
Collaborator

@guggero guggero left a comment

Choose a reason for hiding this comment

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

LGTM, barring Laolu's comments 🎉

@@ -342,7 +340,9 @@ func (s *ScopedKeyManager) loadAccountInfo(ns walletdb.ReadBucket,
nextInternalIndex: row.nextInternalIndex,
}

if !s.rootManager.isLocked() {
watchOnly := s.rootManager.WatchOnly() || len(acctInfo.acctKeyEncrypted) == 0
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we have to be careful where/when we call the exported (WatchOnly, IsLocked) vs the internal 'watchOnly(), isLocked()) methods since the exported ones try to acquire a read lock. I noticed that there is a deadlock when trying to use wallet.DumpPrivKeys() for example. Pretty sure that's pre-existing, but should at least be careful with new code.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the pointer, I fixed a case I introduced.

@Roasbeef
Copy link
Member

Needs a rebase!

@wpaulino
Copy link
Contributor Author

@Roasbeef rebased, though Travis keeps failing. Good thing we have #738 now.

@guggero
Copy link
Collaborator

guggero commented Mar 25, 2021

I've tested this with the lnd PR and a Coldcard. Turns out my PSBT code I added a few months back was incomplete and didn't fully support signing NP2WKH inputs because the redeem script was missing.
Can we add the following patch to this PR to fix it?

diff --git a/np2wkh.patch b/np2wkh.patch
index 36e92d95..e69de29b 100644
--- a/np2wkh.patch
+++ b/np2wkh.patch
@@ -1,147 +0,0 @@
-diff --git a/wallet/psbt.go b/wallet/psbt.go
-index fc9085b4..a0617d82 100644
---- a/wallet/psbt.go
-+++ b/wallet/psbt.go
-@@ -104,9 +104,21 @@ func (w *Wallet) FundPsbt(packet *psbt.Packet, keyScope *waddrmgr.KeyScope,
- 			}
- 
- 			// We don't want to include the witness or any script
--			// just yet.
-+			// on the unsigned TX just yet.
- 			packet.UnsignedTx.TxIn[idx].Witness = wire.TxWitness{}
- 			packet.UnsignedTx.TxIn[idx].SignatureScript = nil
-+
-+			// For nested P2WKH we need to add the redeem script to
-+			// the input, otherwise an offline wallet won't be able
-+			// to sign for it. For normal P2WKH this will be nil.
-+			addr, witnessProgram, _, err := w.scriptForOutput(utxo)
-+			if err != nil {
-+				return fmt.Errorf("error fetching UTXO "+
-+					"script: %v", err)
-+			}
-+			if addr.AddrType() == waddrmgr.NestedWitnessPubKey {
-+				packet.Inputs[idx].RedeemScript = witnessProgram
-+			}
- 		}
- 
- 		return nil
-diff --git a/wallet/signer.go b/wallet/signer.go
-index 390022bf..881c8510 100644
---- a/wallet/signer.go
-+++ b/wallet/signer.go
-@@ -5,6 +5,7 @@
- package wallet
- 
- import (
-+	"fmt"
- 	"github.com/btcsuite/btcd/btcec"
- 	"github.com/btcsuite/btcd/txscript"
- 	"github.com/btcsuite/btcd/wire"
-@@ -12,30 +13,24 @@ import (
- 	"github.com/btcsuite/btcwallet/waddrmgr"
- )
- 
--// PrivKeyTweaker is a function type that can be used to pass in a callback for
--// tweaking a private key before it's used to sign an input.
--type PrivKeyTweaker func(*btcec.PrivateKey) (*btcec.PrivateKey, error)
--
--// ComputeInputScript generates a complete InputScript for the passed
--// transaction with the signature as defined within the passed SignDescriptor.
--// This method is capable of generating the proper input script for both
--// regular p2wkh output and p2wkh outputs nested within a regular p2sh output.
--func (w *Wallet) ComputeInputScript(tx *wire.MsgTx, output *wire.TxOut,
--	inputIndex int, sigHashes *txscript.TxSigHashes,
--	hashType txscript.SigHashType, tweaker PrivKeyTweaker) (wire.TxWitness,
--	[]byte, 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.
-+func (w *Wallet) scriptForOutput(
-+	output *wire.TxOut) (waddrmgr.ManagedPubKeyAddress, []byte, []byte,
-+	error) {
- 
- 	// First make sure we can sign for the input by making sure the script
- 	// in the UTXO belongs to our wallet and we have the private key for it.
- 	walletAddr, err := w.fetchOutputAddr(output.PkScript)
- 	if err != nil {
--		return nil, nil, err
-+		return nil, nil, nil, err
- 	}
- 
--	pka := walletAddr.(waddrmgr.ManagedPubKeyAddress)
--	privKey, err := pka.PrivKey()
--	if err != nil {
--		return nil, nil, err
-+	pubKeyAddr, ok := walletAddr.(waddrmgr.ManagedPubKeyAddress)
-+	if !ok {
-+		return nil, nil, nil, fmt.Errorf("address %s is not a "+
-+			"p2wkh or np2wkh address", walletAddr.Address())
- 	}
- 
- 	var (
-@@ -46,8 +41,8 @@ func (w *Wallet) ComputeInputScript(tx *wire.MsgTx, output *wire.TxOut,
- 	switch {
- 	// If we're spending p2wkh output nested within a p2sh output, then
- 	// we'll need to attach a sigScript in addition to witness data.
--	case pka.AddrType() == waddrmgr.NestedWitnessPubKey:
--		pubKey := privKey.PubKey()
-+	case walletAddr.AddrType() == waddrmgr.NestedWitnessPubKey:
-+		pubKey := pubKeyAddr.PubKey()
- 		pubKeyHash := btcutil.Hash160(pubKey.SerializeCompressed())
- 
- 		// Next, we'll generate a valid sigScript that will allow us to
-@@ -58,18 +53,18 @@ func (w *Wallet) ComputeInputScript(tx *wire.MsgTx, output *wire.TxOut,
- 			pubKeyHash, w.chainParams,
- 		)
- 		if err != nil {
--			return nil, nil, err
-+			return nil, nil, nil, err
- 		}
- 		witnessProgram, err = txscript.PayToAddrScript(p2wkhAddr)
- 		if err != nil {
--			return nil, nil, err
-+			return nil, nil, nil, err
- 		}
- 
- 		bldr := txscript.NewScriptBuilder()
- 		bldr.AddData(witnessProgram)
- 		sigScript, err = bldr.Script()
- 		if err != nil {
--			return nil, nil, err
-+			return nil, nil, nil, err
- 		}
- 
- 	// Otherwise, this is a regular p2wkh output, so we include the
-@@ -81,6 +76,32 @@ func (w *Wallet) ComputeInputScript(tx *wire.MsgTx, output *wire.TxOut,
- 		witnessProgram = output.PkScript
- 	}
- 
-+	return pubKeyAddr, witnessProgram, sigScript, nil
-+}
-+
-+// PrivKeyTweaker is a function type that can be used to pass in a callback for
-+// tweaking a private key before it's used to sign an input.
-+type PrivKeyTweaker func(*btcec.PrivateKey) (*btcec.PrivateKey, error)
-+
-+// ComputeInputScript generates a complete InputScript for the passed
-+// transaction with the signature as defined within the passed SignDescriptor.
-+// This method is capable of generating the proper input script for both
-+// regular p2wkh output and p2wkh outputs nested within a regular p2sh output.
-+func (w *Wallet) ComputeInputScript(tx *wire.MsgTx, output *wire.TxOut,
-+	inputIndex int, sigHashes *txscript.TxSigHashes,
-+	hashType txscript.SigHashType, tweaker PrivKeyTweaker) (wire.TxWitness,
-+	[]byte, error) {
-+
-+	walletAddr, witnessProgram, sigScript, err := w.scriptForOutput(output)
-+	if err != nil {
-+		return nil, nil, err
-+	}
-+
-+	privKey, err := walletAddr.PrivKey()
-+	if err != nil {
-+		return nil, nil, err
-+	}
-+
- 	// If we need to maybe tweak our private key, do it now.
- 	if tweaker != nil {
- 		privKey, err = tweaker(privKey)
diff --git a/wallet/psbt.go b/wallet/psbt.go
index fc9085b4..a0617d82 100644
--- a/wallet/psbt.go
+++ b/wallet/psbt.go
@@ -104,9 +104,21 @@ func (w *Wallet) FundPsbt(packet *psbt.Packet, keyScope *waddrmgr.KeyScope,
 			}
 
 			// We don't want to include the witness or any script
-			// just yet.
+			// on the unsigned TX just yet.
 			packet.UnsignedTx.TxIn[idx].Witness = wire.TxWitness{}
 			packet.UnsignedTx.TxIn[idx].SignatureScript = nil
+
+			// For nested P2WKH we need to add the redeem script to
+			// the input, otherwise an offline wallet won't be able
+			// to sign for it. For normal P2WKH this will be nil.
+			addr, witnessProgram, _, err := w.scriptForOutput(utxo)
+			if err != nil {
+				return fmt.Errorf("error fetching UTXO "+
+					"script: %v", err)
+			}
+			if addr.AddrType() == waddrmgr.NestedWitnessPubKey {
+				packet.Inputs[idx].RedeemScript = witnessProgram
+			}
 		}
 
 		return nil
diff --git a/wallet/signer.go b/wallet/signer.go
index 390022bf..33de184d 100644
--- a/wallet/signer.go
+++ b/wallet/signer.go
@@ -5,6 +5,8 @@
 package wallet
 
 import (
+	"fmt"
+
 	"github.com/btcsuite/btcd/btcec"
 	"github.com/btcsuite/btcd/txscript"
 	"github.com/btcsuite/btcd/wire"
@@ -12,30 +14,24 @@ import (
 	"github.com/btcsuite/btcwallet/waddrmgr"
 )
 
-// PrivKeyTweaker is a function type that can be used to pass in a callback for
-// tweaking a private key before it's used to sign an input.
-type PrivKeyTweaker func(*btcec.PrivateKey) (*btcec.PrivateKey, error)
-
-// ComputeInputScript generates a complete InputScript for the passed
-// transaction with the signature as defined within the passed SignDescriptor.
-// This method is capable of generating the proper input script for both
-// regular p2wkh output and p2wkh outputs nested within a regular p2sh output.
-func (w *Wallet) ComputeInputScript(tx *wire.MsgTx, output *wire.TxOut,
-	inputIndex int, sigHashes *txscript.TxSigHashes,
-	hashType txscript.SigHashType, tweaker PrivKeyTweaker) (wire.TxWitness,
-	[]byte, 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.
+func (w *Wallet) scriptForOutput(
+	output *wire.TxOut) (waddrmgr.ManagedPubKeyAddress, []byte, []byte,
+	error) {
 
 	// First make sure we can sign for the input by making sure the script
 	// in the UTXO belongs to our wallet and we have the private key for it.
 	walletAddr, err := w.fetchOutputAddr(output.PkScript)
 	if err != nil {
-		return nil, nil, err
+		return nil, nil, nil, err
 	}
 
-	pka := walletAddr.(waddrmgr.ManagedPubKeyAddress)
-	privKey, err := pka.PrivKey()
-	if err != nil {
-		return nil, nil, err
+	pubKeyAddr, ok := walletAddr.(waddrmgr.ManagedPubKeyAddress)
+	if !ok {
+		return nil, nil, nil, fmt.Errorf("address %s is not a "+
+			"p2wkh or np2wkh address", walletAddr.Address())
 	}
 
 	var (
@@ -46,8 +42,8 @@ func (w *Wallet) ComputeInputScript(tx *wire.MsgTx, output *wire.TxOut,
 	switch {
 	// If we're spending p2wkh output nested within a p2sh output, then
 	// we'll need to attach a sigScript in addition to witness data.
-	case pka.AddrType() == waddrmgr.NestedWitnessPubKey:
-		pubKey := privKey.PubKey()
+	case walletAddr.AddrType() == waddrmgr.NestedWitnessPubKey:
+		pubKey := pubKeyAddr.PubKey()
 		pubKeyHash := btcutil.Hash160(pubKey.SerializeCompressed())
 
 		// Next, we'll generate a valid sigScript that will allow us to
@@ -58,18 +54,18 @@ func (w *Wallet) ComputeInputScript(tx *wire.MsgTx, output *wire.TxOut,
 			pubKeyHash, w.chainParams,
 		)
 		if err != nil {
-			return nil, nil, err
+			return nil, nil, nil, err
 		}
 		witnessProgram, err = txscript.PayToAddrScript(p2wkhAddr)
 		if err != nil {
-			return nil, nil, err
+			return nil, nil, nil, err
 		}
 
 		bldr := txscript.NewScriptBuilder()
 		bldr.AddData(witnessProgram)
 		sigScript, err = bldr.Script()
 		if err != nil {
-			return nil, nil, err
+			return nil, nil, nil, err
 		}
 
 	// Otherwise, this is a regular p2wkh output, so we include the
@@ -81,6 +77,32 @@ func (w *Wallet) ComputeInputScript(tx *wire.MsgTx, output *wire.TxOut,
 		witnessProgram = output.PkScript
 	}
 
+	return pubKeyAddr, witnessProgram, sigScript, nil
+}
+
+// PrivKeyTweaker is a function type that can be used to pass in a callback for
+// tweaking a private key before it's used to sign an input.
+type PrivKeyTweaker func(*btcec.PrivateKey) (*btcec.PrivateKey, error)
+
+// ComputeInputScript generates a complete InputScript for the passed
+// transaction with the signature as defined within the passed SignDescriptor.
+// This method is capable of generating the proper input script for both
+// regular p2wkh output and p2wkh outputs nested within a regular p2sh output.
+func (w *Wallet) ComputeInputScript(tx *wire.MsgTx, output *wire.TxOut,
+	inputIndex int, sigHashes *txscript.TxSigHashes,
+	hashType txscript.SigHashType, tweaker PrivKeyTweaker) (wire.TxWitness,
+	[]byte, error) {
+
+	walletAddr, witnessProgram, sigScript, err := w.scriptForOutput(output)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	privKey, err := walletAddr.PrivKey()
+	if err != nil {
+		return nil, nil, err
+	}
+
 	// If we need to maybe tweak our private key, do it now.
 	if tweaker != nil {
 		privKey, err = tweaker(privKey)

@Roasbeef
Copy link
Member

Nice find @guggero!

wpaulino and others added 10 commits March 29, 2021 16:00
Watch-only accounts are usually backed by an external hardware signer,
some of which require derivation paths to be populated for each relevant
input to sign.
Following the previous commit, some external hardware signers require a
master key fingerprint to be present within the PSBT input derivation
paths so that the signer can recognize which inputs are relevant and
must be signed.
Now that we're able to fund transactions from multiple accounts within
different key scopes, we extend our transaction creation methods to
accept a key scope parameter as well, to determine the correct account
to select inputs from.
Watch-only accounts don't have any type of private key information
stored, so we avoid populating input signatures in those cases.
This exposes a mapping of account name to its corresponding key scope
and internal account number to facilitate the use of external APIs by
users.
This allows external signers to properly sign NP2WKH inputs.

Co-authored-by: Oliver Gugger <[email protected]>
Copy link
Member

@Roasbeef Roasbeef left a comment

Choose a reason for hiding this comment

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

LGTM 🐠

maxSignedSize := txsizes.EstimateVirtualSize(p2pkh, p2wpkh,
nested, outputs, true)
maxSignedSize := txsizes.EstimateVirtualSize(
p2pkh, p2wpkh, nested, outputs, changeSource.ScriptSize,
Copy link
Member

Choose a reason for hiding this comment

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

👍

@Roasbeef Roasbeef merged commit e060700 into btcsuite:master Mar 29, 2021
@wpaulino wpaulino deleted the watch-only-account-utils branch March 29, 2021 23:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants