diff --git a/waddrmgr/address.go b/waddrmgr/address.go index a162308..d5beb93 100644 --- a/waddrmgr/address.go +++ b/waddrmgr/address.go @@ -47,6 +47,10 @@ const ( // WitnessPubKey represents a p2wkh (pay-to-witness-key-hash) address // type. WitnessPubKey + + // WitnessScript represents a p2wsh (pay-to-witness-script-hash) address + // type. + WitnessScript ) // ManagedAddress is an interface that provides acces to information regarding @@ -54,7 +58,8 @@ const ( // type may provide further fields to provide information specific to that type // of address. type ManagedAddress interface { - // Account returns the internal account the address is associated with. + // InternalAccount returns the internal account the address is + // associated with. InternalAccount() uint32 // Address returns a btcutil.Address for the backing address. @@ -322,7 +327,7 @@ func (a *managedAddress) ExportPrivKey() (*btcutil.WIF, error) { return btcutil.NewWIF(pk, a.manager.rootManager.chainParams, a.compressed) } -// Derivationinfo contains the information required to derive the key that +// DerivationInfo contains the information required to derive the key that // backs the address via traditional methods from the HD root. For imported // keys, the first value will be set to false to indicate that we don't know // exactly how the key was derived. @@ -502,8 +507,9 @@ func newManagedAddressFromExtKey(s *ScopedKeyManager, return managedAddr, nil } -// scriptAddress represents a pay-to-script-hash address. -type scriptAddress struct { +// baseScriptAddress represents the common fields of a pay-to-script-hash and +// a pay-to-witness-script-hash address. +type baseScriptAddress struct { manager *ScopedKeyManager account uint32 address *btcutil.AddressScriptHash @@ -512,14 +518,11 @@ type scriptAddress struct { scriptMutex sync.Mutex } -// Enforce scriptAddress satisfies the ManagedScriptAddress interface. -var _ ManagedScriptAddress = (*scriptAddress)(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 // worrying about it being zeroed during an address lock. -func (a *scriptAddress) unlock(key EncryptorDecryptor) ([]byte, error) { +func (a *baseScriptAddress) unlock(key EncryptorDecryptor) ([]byte, error) { // Protect concurrent access to clear text script. a.scriptMutex.Lock() defer a.scriptMutex.Unlock() @@ -541,7 +544,7 @@ func (a *scriptAddress) unlock(key EncryptorDecryptor) ([]byte, error) { } // lock zeroes the associated clear text script. -func (a *scriptAddress) lock() { +func (a *baseScriptAddress) lock() { // Zero and nil the clear text script associated with this address. a.scriptMutex.Lock() zero.Bytes(a.scriptClearText) @@ -553,10 +556,32 @@ func (a *scriptAddress) lock() { // always be the ImportedAddrAccount constant for script addresses. // // This is part of the ManagedAddress interface implementation. -func (a *scriptAddress) InternalAccount() uint32 { +func (a *baseScriptAddress) InternalAccount() uint32 { return a.account } +// Imported always returns true since script addresses are always imported +// addresses and not part of any chain. +// +// This is part of the ManagedAddress interface implementation. +func (a *baseScriptAddress) Imported() bool { + return true +} + +// Internal always returns false since script addresses are always imported +// addresses and not part of any chain in order to be for internal use. +// +// This is part of the ManagedAddress interface implementation. +func (a *baseScriptAddress) Internal() bool { + return false +} + +// scriptAddress represents a pay-to-script-hash address. +type scriptAddress struct { + baseScriptAddress + address *btcutil.AddressScriptHash +} + // AddrType returns the address type of the managed address. This can be used // to quickly discern the address type without further processing // @@ -580,22 +605,6 @@ func (a *scriptAddress) AddrHash() []byte { return a.address.Hash160()[:] } -// Imported always returns true since script addresses are always imported -// addresses and not part of any chain. -// -// This is part of the ManagedAddress interface implementation. -func (a *scriptAddress) Imported() bool { - return true -} - -// Internal always returns false since script addresses are always imported -// addresses and not part of any chain in order to be for internal use. -// -// This is part of the ManagedAddress interface implementation. -func (a *scriptAddress) Internal() bool { - return false -} - // Compressed returns false since script addresses are never compressed. // // This is part of the ManagedAddress interface implementation. @@ -612,7 +621,7 @@ func (a *scriptAddress) Used(ns walletdb.ReadBucket) bool { // Script returns the script associated with the address. // -// This implements the ScriptAddress interface. +// This is part of the ManagedAddress interface implementation. func (a *scriptAddress) Script() ([]byte, error) { // No script is available for a watching-only address manager. if a.manager.rootManager.WatchOnly() { @@ -633,6 +642,9 @@ func (a *scriptAddress) Script() ([]byte, error) { return a.unlock(a.manager.rootManager.cryptoKeyScript) } +// Enforce scriptAddress satisfies the ManagedScriptAddress interface. +var _ ManagedScriptAddress = (*scriptAddress)(nil) + // newScriptAddress initializes and returns a new pay-to-script-hash address. func newScriptAddress(m *ScopedKeyManager, account uint32, scriptHash, scriptEncrypted []byte) (*scriptAddress, error) { @@ -645,9 +657,131 @@ func newScriptAddress(m *ScopedKeyManager, account uint32, scriptHash, } return &scriptAddress{ - manager: m, - account: account, - address: address, - scriptEncrypted: scriptEncrypted, + baseScriptAddress: baseScriptAddress{ + manager: m, + account: account, + scriptEncrypted: scriptEncrypted, + }, + address: address, + }, nil +} + +// witnessScriptAddress represents a pay-to-witness-script-hash address. +type witnessScriptAddress struct { + baseScriptAddress + address btcutil.Address + + // witnessVersion is the version of the witness script. + witnessVersion byte + + // isSecretScript denotes whether the script is considered to be "secret" + // and encrypted with the script encryption key or "public" and + // therefore only encrypted with the public encryption key. + isSecretScript bool +} + +// 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 *witnessScriptAddress) AddrType() AddressType { + return WitnessScript +} + +// Address returns the btcutil.Address which represents the managed address. +// This will be a pay-to-witness-script-hash address. +// +// This is part of the ManagedAddress interface implementation. +func (a *witnessScriptAddress) Address() btcutil.Address { + return a.address +} + +// AddrHash returns the script hash for the address. +// +// This is part of the ManagedAddress interface implementation. +func (a *witnessScriptAddress) AddrHash() []byte { + return a.address.ScriptAddress() +} + +// Compressed returns true since witness script addresses are always compressed. +// +// This is part of the ManagedAddress interface implementation. +func (a *witnessScriptAddress) Compressed() bool { + return true +} + +// Used returns true if the address has been used in a transaction. +// +// This is part of the ManagedAddress interface implementation. +func (a *witnessScriptAddress) Used(ns walletdb.ReadBucket) bool { + return a.manager.fetchUsed(ns, a.AddrHash()) +} + +// Script returns the script associated with the address. +// +// This is part of the ManagedAddress interface implementation. +func (a *witnessScriptAddress) Script() ([]byte, error) { + // No script is available for a watching-only address manager. + if a.isSecretScript && a.manager.rootManager.WatchOnly() { + return nil, managerError(ErrWatchingOnly, errWatchingOnly, nil) + } + + a.manager.mtx.Lock() + defer a.manager.mtx.Unlock() + + // Account manager must be unlocked to decrypt the script. + if a.isSecretScript && a.manager.rootManager.IsLocked() { + return nil, managerError(ErrLocked, errLocked, nil) + } + + cryptoKey := a.manager.rootManager.cryptoKeyScript + if !a.isSecretScript { + cryptoKey = a.manager.rootManager.cryptoKeyPub + } + + // Decrypt the script as needed. Also, make sure it's a copy since the + // script stored in memory can be cleared at any time. Otherwise, + // the returned script could be invalidated from under the caller. + return a.unlock(cryptoKey) +} + +// Enforce witnessScriptAddress satisfies the ManagedScriptAddress interface. +var _ ManagedScriptAddress = (*witnessScriptAddress)(nil) + +// newWitnessScriptAddress initializes and returns a new +// pay-to-witness-script-hash address. +func newWitnessScriptAddress(m *ScopedKeyManager, account uint32, scriptHash, + scriptEncrypted []byte, witnessVersion byte, + isSecretScript bool) (*witnessScriptAddress, error) { + + var ( + address btcutil.Address + err error + ) + + switch witnessVersion { + case 0x00: + address, err = btcutil.NewAddressWitnessScriptHash( + scriptHash, m.rootManager.chainParams, + ) + + case 0x01: + address, err = btcutil.NewAddressTaproot( + scriptHash, m.rootManager.chainParams, + ) + } + if err != nil { + return nil, err + } + + return &witnessScriptAddress{ + baseScriptAddress: baseScriptAddress{ + manager: m, + account: account, + scriptEncrypted: scriptEncrypted, + }, + address: address, + witnessVersion: witnessVersion, + isSecretScript: isSecretScript, }, nil } diff --git a/waddrmgr/manager_test.go b/waddrmgr/manager_test.go index c324f42..b0a5cf6 100644 --- a/waddrmgr/manager_test.go +++ b/waddrmgr/manager_test.go @@ -6,6 +6,7 @@ package waddrmgr import ( "bytes" + "crypto/sha256" "encoding/hex" "errors" "fmt" @@ -83,16 +84,17 @@ const ( // expectedAddr is used to house the expected return values from a managed // address. Not all fields for used for all managed address types. type expectedAddr struct { - address string - addressHash []byte - internal bool - compressed bool - imported bool - pubKey []byte - privKey []byte - privKeyWIF string - script []byte - derivationInfo DerivationPath + address string + addressHash []byte + internal bool + compressed bool + imported bool + pubKey []byte + privKey []byte + privKeyWIF string + script []byte + derivationInfo DerivationPath + scriptNotSecret bool } // testNamePrefix is a helper to return a prefix to show for test errors based @@ -243,13 +245,16 @@ func testManagedScriptAddress(tc *testContext, prefix string, // the expected error when the manager is locked. gotScript, err := gotAddr.Script() switch { - case tc.watchingOnly: + case tc.watchingOnly && !wantAddr.scriptNotSecret: // Confirm expected watching-only error. testName := fmt.Sprintf("%s Script", prefix) if !checkManagerError(tc.t, testName, err, ErrWatchingOnly) { return false } - case tc.unlocked: + + // Either the manger is unlocked or the script is not considered to + // be secret and is encrypted with the public key. + case tc.unlocked || wantAddr.scriptNotSecret: if err != nil { tc.t.Errorf("%s Script: unexpected error - got %v", prefix, err) @@ -260,6 +265,7 @@ func testManagedScriptAddress(tc *testContext, prefix string, "want %x", prefix, gotScript, wantAddr.script) return false } + default: // Confirm expected locked error. testName := fmt.Sprintf("%s Script", prefix) @@ -883,10 +889,13 @@ func testImportPrivateKey(tc *testContext) bool { // with the manager locked. func testImportScript(tc *testContext) bool { tests := []struct { - name string - in []byte - blockstamp BlockStamp - expected expectedAddr + name string + in []byte + isWitness bool + witnessVersion byte + isSecretScript bool + blockstamp BlockStamp + expected expectedAddr }{ { name: "p2sh uncompressed pubkey", @@ -924,6 +933,64 @@ func testImportScript(tc *testContext) bool { // script is set to the in field during tests. }, }, + { + name: "p2wsh multisig", + isWitness: true, + witnessVersion: 0, + isSecretScript: true, + in: hexToBytes("52210305a662958b547fe25a71cd28fc7ef1c2" + + "ad4a79b12f34fc40137824b88e61199d21038552c09d9" + + "a709c8cbba6e472307d3f8383f46181895a76e01e258f" + + "09033b4a7821029dd72aba87324af59508380f9564d34" + + "b0f7b20d864d186e7d0428c9ea241c61653ae"), + expected: expectedAddr{ + address: "bc1q0jljr70qchwtk3ag0w3gyg9mjhg4c95xr7h8ezhvdrfgppcpz4esfdl9an", + addressHash: hexToBytes("7cbf21f9e0c5dcbb47a87ba28220bb95d15c16861fae7c8aec68d28087011573"), + internal: false, + imported: true, + compressed: true, + // script is set to the in field during tests. + }, + }, + { + name: "p2wsh multisig as watch-only address", + isWitness: true, + witnessVersion: 0, + isSecretScript: false, + in: hexToBytes("52210305a662958b547fe25a71cd28fc7ef1c2" + + "ad4a79b12f34fc40137824b88e61199d21038552c09d9" + + "a709c8cbba6e472307d3f8383f46181895a76e01e258f" + + "09033b4a7821024794b65a83e9ba415096e59abc4d4d1" + + "1710968e52bf5eec56fe0e5bdb3d3ec0e53ae"), + expected: expectedAddr{ + address: "bc1q3a79gkjulrsgp864yskp4d5zmwm49xsdrfwvdypkqtlpj7spd3fqrl5nes", + addressHash: hexToBytes("8f7c545a5cf8e0809f55242c1ab682dbb7529a0d1a5cc6903602fe197a016c52"), + internal: false, + imported: true, + compressed: true, + scriptNotSecret: true, + // script is set to the in field during tests. + }, + }, + { + name: "p2tr multisig", + isWitness: true, + witnessVersion: 1, + isSecretScript: true, + in: hexToBytes("52210305a662958b547fe25a71cd28fc7ef1c2" + + "ad4a79b12f34fc40137824b88e61199d21038552c09d9" + + "a709c8cbba6e472307d3f8383f46181895a76e01e258f" + + "09033b4a78210205ad9a838cff17d79fee2841bec72e9" + + "9b6fd4e62fd9214fcf845b1cf8438062053ae"), + expected: expectedAddr{ + address: "bc1pc57jdm7kcnufnc339fvy2caflj6lkfeqasdfghftl7dd77dfpresqu7vep", + addressHash: hexToBytes("c53d26efd6c4f899e2312a584563a9fcb5fb2720ec1a945d2bff9adf79a908f3"), + internal: false, + imported: true, + compressed: true, + // script is set to the in field during tests. + }, + }, } // The manager must be unlocked to import a private key and also for @@ -955,7 +1022,18 @@ func testImportScript(tc *testContext) bool { err := walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error { ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) var err error - addr, err = tc.manager.ImportScript(ns, test.in, &test.blockstamp) + + if test.isWitness { + addr, err = tc.manager.ImportWitnessScript( + ns, test.in, &test.blockstamp, + test.witnessVersion, + test.isSecretScript, + ) + } else { + addr, err = tc.manager.ImportScript( + ns, test.in, &test.blockstamp, + ) + } return err }) if err != nil { @@ -979,8 +1057,28 @@ func testImportScript(tc *testContext) bool { // Use the Address API to retrieve each of the expected // new addresses and ensure they're accurate. - utilAddr, err := btcutil.NewAddressScriptHash(test.in, - chainParams) + var ( + utilAddr btcutil.Address + err error + ) + switch { + case test.isWitness && test.witnessVersion == 0: + scriptHash := sha256.Sum256(test.in) + utilAddr, err = btcutil.NewAddressWitnessScriptHash( + scriptHash[:], chainParams, + ) + + case test.isWitness && test.witnessVersion == 1: + scriptHash := sha256.Sum256(test.in) + utilAddr, err = btcutil.NewAddressTaproot( + scriptHash[:], chainParams, + ) + + default: + utilAddr, err = btcutil.NewAddressScriptHash( + test.in, chainParams, + ) + } if err != nil { tc.t.Errorf("%s NewAddressScriptHash #%d (%s): "+ "unexpected error: %v", prefix, i, diff --git a/waddrmgr/scoped_manager.go b/waddrmgr/scoped_manager.go index 897b4de..f408cb5 100644 --- a/waddrmgr/scoped_manager.go +++ b/waddrmgr/scoped_manager.go @@ -1,6 +1,7 @@ package waddrmgr import ( + "crypto/sha256" "encoding/binary" "fmt" "sync" @@ -789,7 +790,9 @@ func (s *ScopedKeyManager) importedAddressRowToManaged(row *dbImportedAddressRow // scriptAddressRowToManaged returns a new managed address based on script // address data loaded from the database. -func (s *ScopedKeyManager) scriptAddressRowToManaged(row *dbScriptAddressRow) (ManagedAddress, error) { +func (s *ScopedKeyManager) scriptAddressRowToManaged( + row *dbScriptAddressRow) (ManagedAddress, error) { + // Use the crypto public key to decrypt the imported script hash. scriptHash, err := s.rootManager.cryptoKeyPub.Decrypt(row.encryptedHash) if err != nil { @@ -800,6 +803,24 @@ func (s *ScopedKeyManager) scriptAddressRowToManaged(row *dbScriptAddressRow) (M return newScriptAddress(s, row.account, scriptHash, row.encryptedScript) } +// witnessScriptAddressRowToManaged returns a new managed address based on +// witness script address data loaded from the database. +func (s *ScopedKeyManager) witnessScriptAddressRowToManaged( + row *dbWitnessScriptAddressRow) (ManagedAddress, error) { + + // Use the crypto public key to decrypt the imported script hash. + scriptHash, err := s.rootManager.cryptoKeyPub.Decrypt(row.encryptedHash) + if err != nil { + str := "failed to decrypt imported witness script hash" + return nil, managerError(ErrCrypto, str, err) + } + + return newWitnessScriptAddress( + s, row.account, scriptHash, row.encryptedScript, + row.witnessVersion, row.isSecretScript, + ) +} + // rowInterfaceToManaged returns a new managed address based on the given // address data loaded from the database. It will automatically select the // appropriate type. @@ -817,6 +838,9 @@ func (s *ScopedKeyManager) rowInterfaceToManaged(ns walletdb.ReadBucket, case *dbScriptAddressRow: return s.scriptAddressRowToManaged(row) + + case *dbWitnessScriptAddressRow: + return s.witnessScriptAddressRowToManaged(row) } str := fmt.Sprintf("unsupported address type %T", rowInterface) @@ -2029,16 +2053,63 @@ 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) +} + +// ImportWitnessScript imports a user-provided script into the address manager. +// The imported script will act as a pay-to-witness-script-hash address. +// +// All imported script addresses will be part of the account defined by the +// ImportedAddrAccount constant. +// +// When the address manager is watching-only, the script itself will not be +// stored or available since it is considered private data. +// +// This function will return an error if the address manager is locked and not +// watching-only, or the address already exists. Any other errors returned are +// generally unexpected. +func (s *ScopedKeyManager) ImportWitnessScript(ns walletdb.ReadWriteBucket, + script []byte, bs *BlockStamp, witnessVersion byte, + isSecretScript bool) (ManagedScriptAddress, error) { + + return s.importScriptAddress( + ns, script, bs, WitnessScript, witnessVersion, isSecretScript, + ) +} + +// 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, + witnessVersion byte, isSecretScript bool) (ManagedScriptAddress, + error) { + s.mtx.Lock() defer s.mtx.Unlock() // The manager must be unlocked to encrypt the imported script. - if s.rootManager.IsLocked() { + if isSecretScript && s.rootManager.IsLocked() { return nil, managerError(ErrLocked, errLocked, nil) } + // A secret script can only be used with a non-watch only manager. If + // a wallet is watch-only then the script must be encrypted with the + // public encryption key. + if isSecretScript && s.rootManager.WatchOnly() { + 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 = btcutil.Hash160(script) + } + // Prevent duplicates. - scriptHash := btcutil.Hash160(script) alreadyExists := s.existsAddress(ns, scriptHash) if alreadyExists { str := fmt.Sprintf("address for script hash %x already exists", @@ -2055,18 +2126,21 @@ func (s *ScopedKeyManager) ImportScript(ns walletdb.ReadWriteBucket, return nil, managerError(ErrCrypto, str, err) } - // Encrypt the script for storage in database using the crypto script - // key when not a watching-only address manager. - var encryptedScript []byte - if !s.rootManager.WatchOnly() { - encryptedScript, err = s.rootManager.cryptoKeyScript.Encrypt( - script, - ) - if err != nil { - str := fmt.Sprintf("failed to encrypt script for %x", - scriptHash) - return nil, managerError(ErrCrypto, str, err) - } + // If a key isn't considered to be "secret", we encrypt it with the + // public key, so we can create script addresses that also work in + // watch-only mode. + cryptoKey := s.rootManager.cryptoKeyScript + if !isSecretScript { + cryptoKey = s.rootManager.cryptoKeyPub + } + + // Encrypt the script for storage in database using the selected crypto + // key. + encryptedScript, err := cryptoKey.Encrypt(script) + if err != nil { + str := fmt.Sprintf("failed to encrypt script for %x", + scriptHash) + return nil, managerError(ErrCrypto, str, err) } // The start block needs to be updated when the newly imported address @@ -2080,10 +2154,20 @@ func (s *ScopedKeyManager) ImportScript(ns walletdb.ReadWriteBucket, // Save the new imported address to the db and update start block (if // needed) in a single transaction. - err = putScriptAddress( - ns, &s.scope, scriptHash, ImportedAddrAccount, ssNone, - encryptedHash, encryptedScript, - ) + switch addrType { + case WitnessScript: + err = putWitnessScriptAddress( + ns, &s.scope, scriptHash, ImportedAddrAccount, ssNone, + witnessVersion, isSecretScript, encryptedHash, + encryptedScript, + ) + + default: + err = putScriptAddress( + ns, &s.scope, scriptHash, ImportedAddrAccount, ssNone, + encryptedHash, encryptedScript, + ) + } if err != nil { return nil, maybeConvertDbError(err) } @@ -2107,21 +2191,43 @@ func (s *ScopedKeyManager) ImportScript(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. - scriptAddr, err := newScriptAddress( - s, ImportedAddrAccount, scriptHash, encryptedScript, + var ( + managedAddr ManagedScriptAddress + baseScriptAddr *baseScriptAddress ) - if err != nil { - return nil, err - } - if !s.rootManager.WatchOnly() { - scriptAddr.scriptClearText = make([]byte, len(script)) - copy(scriptAddr.scriptClearText, script) + switch addrType { + case WitnessScript: + witnessAddr, err := newWitnessScriptAddress( + s, ImportedAddrAccount, scriptHash, encryptedScript, + witnessVersion, isSecretScript, + ) + if err != nil { + return nil, err + } + managedAddr = witnessAddr + baseScriptAddr = &witnessAddr.baseScriptAddress + + default: + scriptAddr, err := newScriptAddress( + s, ImportedAddrAccount, scriptHash, encryptedScript, + ) + if err != nil { + return nil, err + } + managedAddr = scriptAddr + baseScriptAddr = &scriptAddr.baseScriptAddress } + // 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) + // Add the new managed address to the cache of recent addresses and // return it. - s.addrs[addrKey(scriptHash)] = scriptAddr - return scriptAddr, nil + s.addrs[addrKey(scriptHash)] = managedAddr + return managedAddr, nil } // lookupAccount loads account number stored in the manager for the given