From 35b96641393f3fb38cc02c6e9e4400b153617bd7 Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 22 Mar 2022 11:27:35 -0700 Subject: [PATCH] Merge pull request #792 from guggero/schnorr-taproot Taproot: Add taproot address and signing capabilities --- waddrmgr/address.go | 159 +++++++++++++++++++++++++++----- waddrmgr/db.go | 17 +++- waddrmgr/manager_test.go | 181 ++++++++++++++++++++++++++++++++++--- waddrmgr/scoped_manager.go | 132 +++++++++++++++++---------- wallet/import.go | 53 +++++++++++ wallet/psbt.go | 33 +++++++ wallet/signer.go | 17 +++- wallet/signer_test.go | 5 +- wallet/txauthor/author.go | 62 +++++++++++-- wallet/wallet.go | 11 ++- 10 files changed, 577 insertions(+), 93 deletions(-) diff --git a/waddrmgr/address.go b/waddrmgr/address.go index 294234a176..1405e56d61 100644 --- a/waddrmgr/address.go +++ b/waddrmgr/address.go @@ -82,6 +82,16 @@ const ( TaprootScript ) +const ( + // witnessVersionV0 is the SegWit v0 witness version used for p2wpkh and + // p2wsh outputs and addresses. + witnessVersionV0 byte = 0x00 + + // witnessVersionV1 is the SegWit v1 witness version used for p2tr + // outputs and addresses. + witnessVersionV1 byte = 0x01 +) + // ManagedAddress is an interface that provides acces to information regarding // an address managed by an address manager. Concrete implementations of this // type may provide further fields to provide information specific to that type @@ -173,6 +183,17 @@ type ManagedScriptAddress interface { Script() ([]byte, error) } +// ManagedTaprootScriptAddress extends ManagedScriptAddress and represents a +// pay-to-taproot script address. It additionally provides information about the +// script. +type ManagedTaprootScriptAddress interface { + ManagedScriptAddress + + // TaprootScript returns all the information needed to derive the script + // tree root hash needed to arrive at the tweaked taproot key. + TaprootScript() (*Tapscript, error) +} + // managedAddress represents a public key address. It also may or may not have // the private key associated with the public key. type managedAddress struct { @@ -557,7 +578,8 @@ func newManagedAddressWithoutPrivKey(m *ScopedKeyManager, case TaprootPubKey: tapKey := txscript.ComputeTaprootKeyNoScript(pubKey) address, err = ltcutil.NewAddressTaproot( - schnorr.SerializePubKey(tapKey), m.rootManager.chainParams, + schnorr.SerializePubKey(tapKey), + m.rootManager.chainParams, ) if err != nil { return nil, err @@ -700,6 +722,14 @@ func newManagedAddressFromExtKey(s *ScopedKeyManager, return managedAddr, nil } +// clearTextScriptSetter is a non-exported interface to identify script types +// that allow their clear text script to be set. +type clearTextScriptSetter interface { + // setClearText sets the unencrypted script on the struct after + // unlocking/decrypting it. + setClearTextScript([]byte) +} + // baseScriptAddress represents the common fields of a pay-to-script-hash and // a pay-to-witness-script-hash address. type baseScriptAddress struct { @@ -711,6 +741,8 @@ type baseScriptAddress struct { scriptMutex sync.Mutex } +var _ clearTextScriptSetter = (*baseScriptAddress)(nil) + // unlock decrypts and stores the associated script. It will fail if the key is // invalid or the encrypted script is not available. The returned clear text // script will always be a copy that may be safely used by the caller without @@ -769,6 +801,13 @@ func (a *baseScriptAddress) Internal() bool { return false } +// setClearText sets the unencrypted script on the struct after unlocking/ +// decrypting it. +func (a *baseScriptAddress) setClearTextScript(script []byte) { + a.scriptClearText = make([]byte, len(script)) + copy(a.scriptClearText, script) +} + // scriptAddress represents a pay-to-script-hash address. type scriptAddress struct { baseScriptAddress @@ -943,38 +982,110 @@ var _ ManagedScriptAddress = (*witnessScriptAddress)(nil) // newWitnessScriptAddress initializes and returns a new // pay-to-witness-script-hash address. -func newWitnessScriptAddress(m *ScopedKeyManager, account uint32, scriptHash, +func newWitnessScriptAddress(m *ScopedKeyManager, account uint32, scriptIdent, scriptEncrypted []byte, witnessVersion byte, - isSecretScript bool) (*witnessScriptAddress, error) { - - var ( - address ltcutil.Address - err error - ) + isSecretScript bool) (ManagedScriptAddress, error) { switch witnessVersion { - case 0x00: - address, err = ltcutil.NewAddressWitnessScriptHash( - scriptHash, m.rootManager.chainParams, + case witnessVersionV0: + address, err := ltcutil.NewAddressWitnessScriptHash( + scriptIdent, m.rootManager.chainParams, ) + if err != nil { + return nil, err + } - case 0x01: - address, err = ltcutil.NewAddressTaproot( - scriptHash, m.rootManager.chainParams, + return &witnessScriptAddress{ + baseScriptAddress: baseScriptAddress{ + manager: m, + account: account, + scriptEncrypted: scriptEncrypted, + }, + address: address, + witnessVersion: witnessVersion, + isSecretScript: isSecretScript, + }, nil + + case witnessVersionV1: + address, err := ltcutil.NewAddressTaproot( + scriptIdent, m.rootManager.chainParams, ) + if err != nil { + return nil, err + } + + // Lift the x-only coordinate of the tweaked public key. + tweakedPubKey, err := schnorr.ParsePubKey(scriptIdent) + if err != nil { + return nil, fmt.Errorf("error lifting public key from "+ + "script ident: %v", err) + } + + return &taprootScriptAddress{ + witnessScriptAddress: witnessScriptAddress{ + baseScriptAddress: baseScriptAddress{ + manager: m, + account: account, + scriptEncrypted: scriptEncrypted, + }, + address: address, + witnessVersion: witnessVersion, + isSecretScript: isSecretScript, + }, + TweakedPubKey: tweakedPubKey, + }, nil + + default: + return nil, fmt.Errorf("unsupported witness version %d", + witnessVersion) } +} + +// taprootScriptAddress represents a pay-to-taproot address that commits to a +// script. +type taprootScriptAddress struct { + witnessScriptAddress + + TweakedPubKey *btcec.PublicKey +} + +// Enforce taprootScriptAddress satisfies the ManagedTaprootScriptAddress +// interface. +var _ ManagedTaprootScriptAddress = (*taprootScriptAddress)(nil) + +// AddrType returns the address type of the managed address. This can be used +// to quickly discern the address type without further processing +// +// This is part of the ManagedAddress interface implementation. +func (a *taprootScriptAddress) AddrType() AddressType { + return TaprootScript +} + +// Address returns the ltcutil.Address which represents the managed address. +// This will be a pay-to-taproot address. +// +// This is part of the ManagedAddress interface implementation. +func (a *taprootScriptAddress) Address() ltcutil.Address { + return a.address +} + +// AddrHash returns the script hash for the address. +// +// This is part of the ManagedAddress interface implementation. +func (a *taprootScriptAddress) AddrHash() []byte { + return schnorr.SerializePubKey(a.TweakedPubKey) +} + +// TaprootScript returns all the information needed to derive the script tree +// root hash needed to arrive at the tweaked taproot key. +func (a *taprootScriptAddress) TaprootScript() (*Tapscript, error) { + // Need to decrypt our internal script first. We need to be unlocked for + // this. + script, err := a.Script() if err != nil { return nil, err } - return &witnessScriptAddress{ - baseScriptAddress: baseScriptAddress{ - manager: m, - account: account, - scriptEncrypted: scriptEncrypted, - }, - address: address, - witnessVersion: witnessVersion, - isSecretScript: isSecretScript, - }, nil + // Decode the additional TLV encoded data. + return tlvDecodeTaprootTaprootScript(script) } diff --git a/waddrmgr/db.go b/waddrmgr/db.go index edc0168c1f..94879e71c7 100644 --- a/waddrmgr/db.go +++ b/waddrmgr/db.go @@ -74,6 +74,7 @@ const ( adtImport addressType = 1 // not iota as they need to be stable for db adtScript addressType = 2 adtWitnessScript addressType = 3 + adtTaprootScript addressType = 4 ) // accountType represents a type of address stored in the database. @@ -1622,6 +1623,11 @@ func fetchAddressByHash(ns walletdb.ReadBucket, scope *KeyScope, return deserializeScriptAddress(row) case adtWitnessScript: return deserializeWitnessScriptAddress(row) + case adtTaprootScript: + // A taproot script address is just a normal script address that + // TLV encodes more stuff in the raw script part. But in the + // database we store the same fields. + return deserializeWitnessScriptAddress(row) } str := fmt.Sprintf("unsupported address type '%d'", row.addrType) @@ -1849,8 +1855,17 @@ func putWitnessScriptAddress(ns walletdb.ReadWriteBucket, scope *KeyScope, rawData := serializeWitnessScriptAddress( witnessVersion, isSecretScript, encryptedHash, encryptedScript, ) + + addrType := adtWitnessScript + if witnessVersion == witnessVersionV1 { + // A taproot script stores a TLV encoded blob of data in the + // raw data field. So we only really need to use a different + // storage type since all other fields stay the same. + addrType = adtTaprootScript + } + addrRow := dbAddressRow{ - addrType: adtWitnessScript, + addrType: addrType, account: account, addTime: uint64(time.Now().Unix()), syncStatus: status, diff --git a/waddrmgr/manager_test.go b/waddrmgr/manager_test.go index f2f77dc3b4..c731483a0f 100644 --- a/waddrmgr/manager_test.go +++ b/waddrmgr/manager_test.go @@ -19,6 +19,7 @@ import ( "github.com/dcrlabs/ltcwallet/snacl" "github.com/dcrlabs/ltcwallet/walletdb" "github.com/ltcsuite/ltcd/btcec/v2" + "github.com/ltcsuite/ltcd/btcec/v2/schnorr" "github.com/ltcsuite/ltcd/chaincfg" "github.com/ltcsuite/ltcd/chaincfg/chainhash" "github.com/ltcsuite/ltcd/ltcutil" @@ -892,6 +893,7 @@ func testImportScript(tc *testContext) bool { name string in []byte isWitness bool + isTaproot bool witnessVersion byte isSecretScript bool blockstamp BlockStamp @@ -973,15 +975,20 @@ func testImportScript(tc *testContext) bool { }, }, { - name: "p2tr multisig", - isWitness: true, + name: "p2tr tapscript with all tap leaves", + isTaproot: true, witnessVersion: 1, isSecretScript: true, - in: hexToBytes("52210305a662958b547fe25a71cd28fc7ef1c2" + - "ad4a79b12f34fc40137824b88e61199d21038552c09d9" + - "a709c8cbba6e472307d3f8383f46181895a76e01e258f" + - "09033b4a78210205ad9a838cff17d79fee2841bec72e9" + - "9b6fd4e62fd9214fcf845b1cf8438062053ae"), + // The encoded *Tapscript struct for a script with all + // tap script leaves known. + in: hexToBytes( + "0101000221c00ef94ee79c07cbd1988fffd6e6aea1e2" + + "5c3b033a2fd64fe14a9b955e5355f0c60346" + + "1d0101c0021876a914f6c97547d73156abb3" + + "00ae059905c4acaadd09dd88270101c00222" + + "200ef94ee79c07cbd1988fffd6e6aea1e25c" + + "3b033a2fd64fe14a9b955e5355f0c6ac", + ), expected: expectedAddr{ address: "ltc1pc57jdm7kcnufnc339fvy2caflj6lkfeqasdfghftl7dd77dfpresrcsury", addressHash: hexToBytes("c53d26efd6c4f899e2312a584563a9fcb5fb2720ec1a945d2bff9adf79a908f3"), @@ -1023,13 +1030,27 @@ func testImportScript(tc *testContext) bool { ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) var err error - if test.isWitness { + switch { + case test.isWitness: addr, err = tc.manager.ImportWitnessScript( ns, test.in, &test.blockstamp, test.witnessVersion, test.isSecretScript, ) - } else { + + case test.isTaproot: + var script *Tapscript + script, err = tlvDecodeTaprootTaprootScript( + test.in, + ) + require.NoError(tc.t, err) + addr, err = tc.manager.ImportTaprootScript( + ns, script, &test.blockstamp, + test.witnessVersion, + test.isSecretScript, + ) + + default: addr, err = tc.manager.ImportScript( ns, test.in, &test.blockstamp, ) @@ -1068,10 +1089,21 @@ func testImportScript(tc *testContext) bool { scriptHash[:], chainParams, ) - case test.isWitness && test.witnessVersion == 1: - scriptHash := sha256.Sum256(test.in) + case test.isTaproot: + var ( + script *Tapscript + taprootKey *btcec.PublicKey + ) + script, err = tlvDecodeTaprootTaprootScript( + test.in, + ) + require.NoError(tc.t, err) + taprootKey, err = script.TaprootKey() + require.NoError(tc.t, err) + utilAddr, err = ltcutil.NewAddressTaproot( - scriptHash[:], chainParams, + schnorr.SerializePubKey(taprootKey), + chainParams, ) default: @@ -3272,3 +3304,128 @@ func TestManagedAddressValidation(t *testing.T) { } } + +// TestTaprootPubKeyDerivation tests that p2tr addresses can be derived from the +// scoped manager when using the BIP0086 key scope. +func TestTaprootPubKeyDerivation(t *testing.T) { + t.Parallel() + + teardown, db := emptyDB(t) + defer teardown() + + // From: https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki + rootKey, _ := hdkeychain.NewKeyFromString( + "xprv9s21ZrQH143K3GJpoapnV8SFfukcVBSfeCficPSGfubmSFDxo1kuHnLi" + + "sriDvSnRRuL2Qrg5ggqHKNVpxR86QEC8w35uxmGoggxtQTPvfUu", + ) + + // We'll start the test by creating a new root manager that will be + // used for the duration of the test. + var mgr *Manager + err := walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { + ns, err := tx.CreateTopLevelBucket(waddrmgrNamespaceKey) + if err != nil { + return err + } + err = Create( + ns, rootKey, pubPassphrase, privPassphrase, + &chaincfg.MainNetParams, fastScrypt, time.Time{}, + ) + if err != nil { + return err + } + mgr, err = Open(ns, pubPassphrase, &chaincfg.MainNetParams) + if err != nil { + return err + } + + return mgr.Unlock(ns, privPassphrase) + }) + require.NoError(t, err, "create/open: unexpected error: %v", err) + + defer mgr.Close() + + // Now that we have the manager created, we'll fetch one of the default + // scopes for usage within this test. + scopedMgr, err := mgr.FetchScopedKeyManager(KeyScopeBIP0086) + require.NoError( + t, err, "unable to fetch scope %v: %v", KeyScopeBIP0086, err, + ) + + externalPath := DerivationPath{ + InternalAccount: 0, + Account: hdkeychain.HardenedKeyStart, + Branch: 0, + Index: 0, + } + internalPath := DerivationPath{ + InternalAccount: 0, + Account: hdkeychain.HardenedKeyStart, + Branch: 1, + Index: 0, + } + + assertAddressDerivation( + t, db, func(ns walletdb.ReadWriteBucket) (ManagedAddress, error) { + return scopedMgr.DeriveFromKeyPath(ns, externalPath) + }, + "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr", + ) + assertAddressDerivation( + t, db, func(ns walletdb.ReadWriteBucket) (ManagedAddress, error) { + addrs, err := scopedMgr.NextExternalAddresses(ns, 0, 1) + if err != nil { + return nil, err + } + return addrs[0], nil + }, + "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr", + ) + assertAddressDerivation( + t, db, func(ns walletdb.ReadWriteBucket) (ManagedAddress, error) { + return scopedMgr.LastExternalAddress(ns, 0) + }, + "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr", + ) + assertAddressDerivation( + t, db, func(ns walletdb.ReadWriteBucket) (ManagedAddress, error) { + return scopedMgr.DeriveFromKeyPath(ns, internalPath) + }, + "bc1p3qkhfews2uk44qtvauqyr2ttdsw7svhkl9nkm9s9c3x4ax5h60wqwruhk7", + ) + assertAddressDerivation( + t, db, func(ns walletdb.ReadWriteBucket) (ManagedAddress, error) { + addrs, err := scopedMgr.NextInternalAddresses(ns, 0, 1) + if err != nil { + return nil, err + } + return addrs[0], nil + }, + "bc1p3qkhfews2uk44qtvauqyr2ttdsw7svhkl9nkm9s9c3x4ax5h60wqwruhk7", + ) + assertAddressDerivation( + t, db, func(ns walletdb.ReadWriteBucket) (ManagedAddress, error) { + return scopedMgr.LastInternalAddress(ns, 0) + }, + "bc1p3qkhfews2uk44qtvauqyr2ttdsw7svhkl9nkm9s9c3x4ax5h60wqwruhk7", + ) +} + +// assertAddressDerivation makes sure the address derived in the given callback +// is the one that is expected. +func assertAddressDerivation(t *testing.T, db walletdb.DB, + fn func(walletdb.ReadWriteBucket) (ManagedAddress, error), + expectedAddr string) { + + var address ManagedAddress + err := walletdb.Update(db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + + var err error + address, err = fn(ns) + return err + }) + require.NoError(t, err, "unable to derive addr: %v", err) + + require.Equal(t, expectedAddr, address.Address().String()) +} diff --git a/waddrmgr/scoped_manager.go b/waddrmgr/scoped_manager.go index 42457e358b..59fffd6f98 100644 --- a/waddrmgr/scoped_manager.go +++ b/waddrmgr/scoped_manager.go @@ -131,6 +131,31 @@ func (k KeyScope) String() string { return fmt.Sprintf("m/%v'/%v'", k.Purpose, k.Coin) } +// Identity is a closure that returns the identifier of an address. +type Identity func() []byte + +// ScriptHashIdentity returns the identity closure for a p2sh script. +func ScriptHashIdentity(script []byte) Identity { + return func() []byte { + return ltcutil.Hash160(script) + } +} + +// WitnessScriptHashIdentity returns the identity closure for a p2wsh script. +func WitnessScriptHashIdentity(script []byte) Identity { + return func() []byte { + digest := sha256.Sum256(script) + return digest[:] + } +} + +// TaprootIdentity returns the identity closure for a p2tr script. +func TaprootIdentity(taprootKey *btcec.PublicKey) Identity { + return func() []byte { + return schnorr.SerializePubKey(taprootKey) + } +} + // ScopeAddrSchema is the address schema of a particular KeyScope. This will be // persisted within the database, and will be consulted when deriving any keys // for a particular scope to know how to encode the public keys as addresses. @@ -2148,7 +2173,9 @@ func (s *ScopedKeyManager) toImportedPublicManagedAddress( func (s *ScopedKeyManager) ImportScript(ns walletdb.ReadWriteBucket, script []byte, bs *BlockStamp) (ManagedScriptAddress, error) { - return s.importScriptAddress(ns, script, bs, Script, 0, true) + return s.importScriptAddress( + ns, ScriptHashIdentity(script), script, bs, Script, 0, true, + ) } // ImportWitnessScript imports a user-provided script into the address manager. @@ -2168,14 +2195,45 @@ func (s *ScopedKeyManager) ImportWitnessScript(ns walletdb.ReadWriteBucket, isSecretScript bool) (ManagedScriptAddress, error) { return s.importScriptAddress( - ns, script, bs, WitnessScript, witnessVersion, isSecretScript, + ns, WitnessScriptHashIdentity(script), script, bs, + WitnessScript, witnessVersion, isSecretScript, ) } +// ImportTaprootScript imports a user-provided taproot script into the address +// manager. The imported script will act as a pay-to-taproot address. +func (s *ScopedKeyManager) ImportTaprootScript(ns walletdb.ReadWriteBucket, + tapscript *Tapscript, bs *BlockStamp, witnessVersion byte, + isSecretScript bool) (ManagedTaprootScriptAddress, error) { + + // Make sure we have everything we need to calculate the script root and + // tweak the taproot key. + taprootKey, err := tapscript.TaprootKey() + if err != nil { + return nil, fmt.Errorf("error calculating script root: %v", err) + } + + script, err := tlvEncodeTaprootScript(tapscript) + if err != nil { + return nil, fmt.Errorf("error encoding taproot script: %v", err) + } + + managedAddr, err := s.importScriptAddress( + ns, TaprootIdentity(taprootKey), script, bs, + TaprootScript, witnessVersion, isSecretScript, + ) + if err != nil { + return nil, err + } + + // We know this is a taproot address at this point. + return managedAddr.(ManagedTaprootScriptAddress), nil +} + // importScriptAddress imports a new pay-to-script or pay-to-witness-script // address. func (s *ScopedKeyManager) importScriptAddress(ns walletdb.ReadWriteBucket, - script []byte, bs *BlockStamp, addrType AddressType, + identity Identity, script []byte, bs *BlockStamp, addrType AddressType, witnessVersion byte, isSecretScript bool) (ManagedScriptAddress, error) { @@ -2194,30 +2252,21 @@ func (s *ScopedKeyManager) importScriptAddress(ns walletdb.ReadWriteBucket, return nil, managerError(ErrWatchingOnly, errWatchingOnly, nil) } - // Witness script addresses use a SHA256. - var scriptHash []byte - switch addrType { - case WitnessScript: - digest := sha256.Sum256(script) - scriptHash = digest[:] - default: - scriptHash = ltcutil.Hash160(script) - } - // Prevent duplicates. - alreadyExists := s.existsAddress(ns, scriptHash) + scriptIdent := identity() + alreadyExists := s.existsAddress(ns, scriptIdent) if alreadyExists { - str := fmt.Sprintf("address for script hash %x already exists", - scriptHash) + str := fmt.Sprintf("address for script hash/key %x already "+ + "exists", scriptIdent) return nil, managerError(ErrDuplicateAddress, str, nil) } - // Encrypt the script hash using the crypto public key so it is + // Encrypt the script hash/key using the crypto public key, so it is // accessible when the address manager is locked or watching-only. - encryptedHash, err := s.rootManager.cryptoKeyPub.Encrypt(scriptHash) + encryptedHash, err := s.rootManager.cryptoKeyPub.Encrypt(scriptIdent) if err != nil { - str := fmt.Sprintf("failed to encrypt script hash %x", - scriptHash) + str := fmt.Sprintf("failed to encrypt script hash/key %x", + scriptIdent) return nil, managerError(ErrCrypto, str, err) } @@ -2234,7 +2283,7 @@ func (s *ScopedKeyManager) importScriptAddress(ns walletdb.ReadWriteBucket, encryptedScript, err := cryptoKey.Encrypt(script) if err != nil { str := fmt.Sprintf("failed to encrypt script for %x", - scriptHash) + scriptIdent) return nil, managerError(ErrCrypto, str, err) } @@ -2250,16 +2299,16 @@ func (s *ScopedKeyManager) importScriptAddress(ns walletdb.ReadWriteBucket, // Save the new imported address to the db and update start block (if // needed) in a single transaction. switch addrType { - case WitnessScript: + case WitnessScript, TaprootScript: err = putWitnessScriptAddress( - ns, &s.scope, scriptHash, ImportedAddrAccount, ssNone, + ns, &s.scope, scriptIdent, ImportedAddrAccount, ssNone, witnessVersion, isSecretScript, encryptedHash, encryptedScript, ) default: err = putScriptAddress( - ns, &s.scope, scriptHash, ImportedAddrAccount, ssNone, + ns, &s.scope, scriptIdent, ImportedAddrAccount, ssNone, encryptedHash, encryptedScript, ) } @@ -2286,42 +2335,33 @@ func (s *ScopedKeyManager) importScriptAddress(ns walletdb.ReadWriteBucket, // when not a watching-only address manager, make a copy of the script // since it will be cleared on lock and the script the caller passed // should not be cleared out from under the caller. - var ( - managedAddr ManagedScriptAddress - baseScriptAddr *baseScriptAddress - ) + var managedAddr ManagedScriptAddress switch addrType { - case WitnessScript: - witnessAddr, err := newWitnessScriptAddress( - s, ImportedAddrAccount, scriptHash, encryptedScript, + case WitnessScript, TaprootScript: + managedAddr, err = newWitnessScriptAddress( + s, ImportedAddrAccount, scriptIdent, encryptedScript, witnessVersion, isSecretScript, ) - if err != nil { - return nil, err - } - managedAddr = witnessAddr - baseScriptAddr = &witnessAddr.baseScriptAddress default: - scriptAddr, err := newScriptAddress( - s, ImportedAddrAccount, scriptHash, encryptedScript, + managedAddr, err = newScriptAddress( + s, ImportedAddrAccount, scriptIdent, encryptedScript, ) - if err != nil { - return nil, err - } - managedAddr = scriptAddr - baseScriptAddr = &scriptAddr.baseScriptAddress + } + if err != nil { + return nil, err } // Even if the script is secret, we are currently unlocked, so we keep a // clear text copy of the script around to avoid decrypting it on each // access. - baseScriptAddr.scriptClearText = make([]byte, len(script)) - copy(baseScriptAddr.scriptClearText, script) + if cts, ok := managedAddr.(clearTextScriptSetter); ok { + cts.setClearTextScript(script) + } // Add the new managed address to the cache of recent addresses and // return it. - s.addrs[addrKey(scriptHash)] = managedAddr + s.addrs[addrKey(scriptIdent)] = managedAddr return managedAddr, nil } diff --git a/wallet/import.go b/wallet/import.go index c22725ab73..70a2e17ac1 100644 --- a/wallet/import.go +++ b/wallet/import.go @@ -427,6 +427,59 @@ func (w *Wallet) ImportPublicKey(pubKey *btcec.PublicKey, return nil } +// ImportTaprootScript imports a user-provided taproot script into the address +// manager. The imported script will act as a pay-to-taproot address. +func (w *Wallet) ImportTaprootScript(scope waddrmgr.KeyScope, + tapscript *waddrmgr.Tapscript, bs *waddrmgr.BlockStamp, + witnessVersion byte, isSecretScript bool) (waddrmgr.ManagedAddress, + error) { + + manager, err := w.Manager.FetchScopedKeyManager(scope) + if err != nil { + return nil, err + } + + // The starting block for the key is the genesis block unless otherwise + // specified. + if bs == nil { + bs = &waddrmgr.BlockStamp{ + Hash: *w.chainParams.GenesisHash, + Height: 0, + Timestamp: w.chainParams.GenesisBlock.Header.Timestamp, + } + } else if bs.Timestamp.IsZero() { + // Only update the new birthday time from default value if we + // actually have timestamp info in the header. + header, err := w.chainClient.GetBlockHeader(&bs.Hash) + if err == nil { + bs.Timestamp = header.Timestamp + } + } + + // TODO: Perform rescan if requested. + var addr waddrmgr.ManagedAddress + err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + addr, err = manager.ImportTaprootScript( + ns, tapscript, bs, witnessVersion, isSecretScript, + ) + return err + }) + if err != nil { + return nil, err + } + + log.Infof("Imported address %v", addr.Address()) + + err = w.chainClient.NotifyReceived([]ltcutil.Address{addr.Address()}) + if err != nil { + return nil, fmt.Errorf("unable to subscribe for address "+ + "notifications: %v", err) + } + + return addr, nil +} + // ImportPrivateKey imports a private key to the wallet and writes the new // wallet to disk. // diff --git a/wallet/psbt.go b/wallet/psbt.go index dde4186bac..bb326c2f5d 100644 --- a/wallet/psbt.go +++ b/wallet/psbt.go @@ -535,6 +535,39 @@ func (w *Wallet) FinalizePsbt(keyScope *waddrmgr.KeyScope, account uint32, return nil } +// PsbtPrevOutputFetcher returns a txscript.PrevOutFetcher built from the UTXO +// information in a PSBT packet. +func PsbtPrevOutputFetcher(packet *psbt.Packet) *txscript.MultiPrevOutFetcher { + fetcher := txscript.NewMultiPrevOutFetcher(nil) + for idx, txIn := range packet.UnsignedTx.TxIn { + in := packet.Inputs[idx] + + // Skip any input that has no UTXO. + if in.WitnessUtxo == nil && in.NonWitnessUtxo == nil { + continue + } + + if in.NonWitnessUtxo != nil { + prevIndex := txIn.PreviousOutPoint.Index + fetcher.AddPrevOut( + txIn.PreviousOutPoint, + in.NonWitnessUtxo.TxOut[prevIndex], + ) + + continue + } + + // Fall back to witness UTXO only for older wallets. + if in.WitnessUtxo != nil { + fetcher.AddPrevOut( + txIn.PreviousOutPoint, in.WitnessUtxo, + ) + } + } + + return fetcher +} + // constantInputSource creates an input source function that always returns the // static set of user-selected UTXOs. func constantInputSource(eligible []wtxmgr.Credit) txauthor.InputSource { diff --git a/wallet/signer.go b/wallet/signer.go index bb2f696b65..6197c7b745 100644 --- a/wallet/signer.go +++ b/wallet/signer.go @@ -67,7 +67,7 @@ func (w *Wallet) ScriptForOutput(output *wire.TxOut) ( return nil, nil, nil, err } - // Otherwise, this is a regular p2wkh output, so we include the + // Otherwise, this is a regular p2wkh or p2tr output, so we include the // witness program itself as the subscript to generate the proper // sighash digest. As part of the new sighash digest algorithm, the // p2wkh witness program will be expanded into a regular p2kh @@ -110,6 +110,21 @@ func (w *Wallet) ComputeInputScript(tx *wire.MsgTx, output *wire.TxOut, } } + // We need to produce a Schnorr signature for p2tr key spend addresses. + if txscript.IsPayToTaproot(output.PkScript) { + // We can now generate a valid witness which will allow us to + // spend this output. + witnessScript, err := txscript.TaprootWitnessSignature( + tx, sigHashes, inputIndex, output.Value, + output.PkScript, hashType, privKey, + ) + if err != nil { + return nil, nil, err + } + + return witnessScript, nil, nil + } + // Generate a valid witness stack for the input. witnessScript, err := txscript.WitnessSignature( tx, sigHashes, inputIndex, output.Value, witnessProgram, diff --git a/wallet/signer_test.go b/wallet/signer_test.go index 38307ba1cc..6bcc393972 100644 --- a/wallet/signer_test.go +++ b/wallet/signer_test.go @@ -76,7 +76,10 @@ func runTestCase(t *testing.T, w *Wallet, scope waddrmgr.KeyScope, }}, TxOut: []*wire.TxOut{utxOut}, } - sigHashes := txscript.NewTxSigHashes(outgoingTx, new(txscript.CannedPrevOutputFetcher)) + fetcher := txscript.NewCannedPrevOutputFetcher( + utxOut.PkScript, utxOut.Value, + ) + sigHashes := txscript.NewTxSigHashes(outgoingTx, fetcher) // Compute the input script to spend the UTXO now. witness, script, err := w.ComputeInputScript( diff --git a/wallet/txauthor/author.go b/wallet/txauthor/author.go index a061f2af0e..732020d125 100644 --- a/wallet/txauthor/author.go +++ b/wallet/txauthor/author.go @@ -231,19 +231,32 @@ func AddAllInputScripts(tx *wire.MsgTx, prevPkScripts [][]byte, // function which generates both the sigScript, and the witness // script. case txscript.IsPayToScriptHash(pkScript): - err := spendNestedWitnessPubKeyHash(inputs[i], pkScript, - int64(inputValues[i]), chainParams, secrets, - tx, hashCache, i) + err := spendNestedWitnessPubKeyHash( + inputs[i], pkScript, int64(inputValues[i]), + chainParams, secrets, tx, hashCache, i, + ) if err != nil { return err } + case txscript.IsPayToWitnessPubKeyHash(pkScript): - err := spendWitnessKeyHash(inputs[i], pkScript, - int64(inputValues[i]), chainParams, secrets, - tx, hashCache, i) + err := spendWitnessKeyHash( + inputs[i], pkScript, int64(inputValues[i]), + chainParams, secrets, tx, hashCache, i, + ) if err != nil { return err } + + case txscript.IsPayToTaproot(pkScript): + err := spendTaprootKey( + inputs[i], pkScript, int64(inputValues[i]), + chainParams, secrets, tx, hashCache, i, + ) + if err != nil { + return err + } + default: sigScript := inputs[i].SignatureScript script, err := txscript.SignTxOutput(chainParams, tx, i, @@ -311,6 +324,43 @@ func spendWitnessKeyHash(txIn *wire.TxIn, pkScript []byte, return nil } +// spendTaprootKey generates, and sets a valid witness for spending the passed +// pkScript with the specified input amount. The input amount *must* +// correspond to the output value of the previous pkScript, or else verification +// will fail since the new sighash digest algorithm defined in BIP0341 includes +// the input value in the sighash. +func spendTaprootKey(txIn *wire.TxIn, pkScript []byte, + inputValue int64, chainParams *chaincfg.Params, secrets SecretsSource, + tx *wire.MsgTx, hashCache *txscript.TxSigHashes, idx int) error { + + // First obtain the key pair associated with this p2tr address. If the + // pkScript is incorrect or derived from a different internal key or + // with a script root, we simply won't find a corresponding private key + // here. + _, addrs, _, err := txscript.ExtractPkScriptAddrs(pkScript, chainParams) + if err != nil { + return err + } + privKey, _, err := secrets.GetKey(addrs[0]) + if err != nil { + return err + } + + // We can now generate a valid witness which will allow us to spend this + // output. + witnessScript, err := txscript.TaprootWitnessSignature( + tx, hashCache, idx, inputValue, pkScript, + txscript.SigHashDefault, privKey, + ) + if err != nil { + return err + } + + txIn.Witness = witnessScript + + return nil +} + // spendNestedWitnessPubKey generates both a sigScript, and valid witness for // spending the passed pkScript with the specified input amount. The generated // sigScript is the version 0 p2wkh witness program corresponding to the queried diff --git a/wallet/wallet.go b/wallet/wallet.go index 83b9a6fbce..a387899b87 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -3454,6 +3454,7 @@ func (w *Wallet) SignTransaction(tx *wire.MsgTx, hashType txscript.SigHashType, addrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey) txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey) + inputFetcher := txscript.NewMultiPrevOutFetcher(nil) for i, txIn := range tx.TxIn { prevOutScript, ok := additionalPrevScripts[txIn.PreviousOutPoint] if !ok { @@ -3470,6 +3471,9 @@ func (w *Wallet) SignTransaction(tx *wire.MsgTx, hashType txscript.SigHashType, } prevOutScript = txDetails.MsgTx.TxOut[prevIndex].PkScript } + inputFetcher.AddPrevOut(txIn.PreviousOutPoint, &wire.TxOut{ + PkScript: prevOutScript, + }) // Set up our callbacks that we pass to txscript so it can // look up the appropriate keys and scripts by address. @@ -3548,8 +3552,11 @@ func (w *Wallet) SignTransaction(tx *wire.MsgTx, hashType txscript.SigHashType, // Either it was already signed or we just signed it. // Find out if it is completely satisfied or still needs more. - vm, err := txscript.NewEngine(prevOutScript, tx, i, - txscript.StandardVerifyFlags, nil, nil, 0, new(txscript.CannedPrevOutputFetcher)) + vm, err := txscript.NewEngine( + prevOutScript, tx, i, + txscript.StandardVerifyFlags, nil, nil, 0, + inputFetcher, + ) if err == nil { err = vm.Execute() }