waddrmgr: add support for pay to witness script address

With this commit we refactor the existing script address into a
baseScriptAddress struct and then add a new witnessScriptAddress type
that manages a pay-to-witness-script address.
This commit is contained in:
Yaacov Akiba Slama 2021-12-02 15:10:24 +01:00 committed by Roy Lee
parent 9eb48cb6ab
commit 759741dccc
3 changed files with 417 additions and 79 deletions

View file

@ -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
}

View file

@ -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,

View file

@ -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