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:
parent
9eb48cb6ab
commit
759741dccc
3 changed files with 417 additions and 79 deletions
|
@ -47,6 +47,10 @@ const (
|
||||||
// WitnessPubKey represents a p2wkh (pay-to-witness-key-hash) address
|
// WitnessPubKey represents a p2wkh (pay-to-witness-key-hash) address
|
||||||
// type.
|
// type.
|
||||||
WitnessPubKey
|
WitnessPubKey
|
||||||
|
|
||||||
|
// WitnessScript represents a p2wsh (pay-to-witness-script-hash) address
|
||||||
|
// type.
|
||||||
|
WitnessScript
|
||||||
)
|
)
|
||||||
|
|
||||||
// ManagedAddress is an interface that provides acces to information regarding
|
// 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
|
// type may provide further fields to provide information specific to that type
|
||||||
// of address.
|
// of address.
|
||||||
type ManagedAddress interface {
|
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
|
InternalAccount() uint32
|
||||||
|
|
||||||
// Address returns a btcutil.Address for the backing address.
|
// 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)
|
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
|
// 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
|
// keys, the first value will be set to false to indicate that we don't know
|
||||||
// exactly how the key was derived.
|
// exactly how the key was derived.
|
||||||
|
@ -502,8 +507,9 @@ func newManagedAddressFromExtKey(s *ScopedKeyManager,
|
||||||
return managedAddr, nil
|
return managedAddr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// scriptAddress represents a pay-to-script-hash address.
|
// baseScriptAddress represents the common fields of a pay-to-script-hash and
|
||||||
type scriptAddress struct {
|
// a pay-to-witness-script-hash address.
|
||||||
|
type baseScriptAddress struct {
|
||||||
manager *ScopedKeyManager
|
manager *ScopedKeyManager
|
||||||
account uint32
|
account uint32
|
||||||
address *btcutil.AddressScriptHash
|
address *btcutil.AddressScriptHash
|
||||||
|
@ -512,14 +518,11 @@ type scriptAddress struct {
|
||||||
scriptMutex sync.Mutex
|
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
|
// 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
|
// 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
|
// script will always be a copy that may be safely used by the caller without
|
||||||
// worrying about it being zeroed during an address lock.
|
// 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.
|
// Protect concurrent access to clear text script.
|
||||||
a.scriptMutex.Lock()
|
a.scriptMutex.Lock()
|
||||||
defer a.scriptMutex.Unlock()
|
defer a.scriptMutex.Unlock()
|
||||||
|
@ -541,7 +544,7 @@ func (a *scriptAddress) unlock(key EncryptorDecryptor) ([]byte, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// lock zeroes the associated clear text script.
|
// 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.
|
// Zero and nil the clear text script associated with this address.
|
||||||
a.scriptMutex.Lock()
|
a.scriptMutex.Lock()
|
||||||
zero.Bytes(a.scriptClearText)
|
zero.Bytes(a.scriptClearText)
|
||||||
|
@ -553,10 +556,32 @@ func (a *scriptAddress) lock() {
|
||||||
// always be the ImportedAddrAccount constant for script addresses.
|
// always be the ImportedAddrAccount constant for script addresses.
|
||||||
//
|
//
|
||||||
// This is part of the ManagedAddress interface implementation.
|
// This is part of the ManagedAddress interface implementation.
|
||||||
func (a *scriptAddress) InternalAccount() uint32 {
|
func (a *baseScriptAddress) InternalAccount() uint32 {
|
||||||
return a.account
|
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
|
// AddrType returns the address type of the managed address. This can be used
|
||||||
// to quickly discern the address type without further processing
|
// to quickly discern the address type without further processing
|
||||||
//
|
//
|
||||||
|
@ -580,22 +605,6 @@ func (a *scriptAddress) AddrHash() []byte {
|
||||||
return a.address.Hash160()[:]
|
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.
|
// Compressed returns false since script addresses are never compressed.
|
||||||
//
|
//
|
||||||
// This is part of the ManagedAddress interface implementation.
|
// 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.
|
// 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) {
|
func (a *scriptAddress) Script() ([]byte, error) {
|
||||||
// No script is available for a watching-only address manager.
|
// No script is available for a watching-only address manager.
|
||||||
if a.manager.rootManager.WatchOnly() {
|
if a.manager.rootManager.WatchOnly() {
|
||||||
|
@ -633,6 +642,9 @@ func (a *scriptAddress) Script() ([]byte, error) {
|
||||||
return a.unlock(a.manager.rootManager.cryptoKeyScript)
|
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.
|
// newScriptAddress initializes and returns a new pay-to-script-hash address.
|
||||||
func newScriptAddress(m *ScopedKeyManager, account uint32, scriptHash,
|
func newScriptAddress(m *ScopedKeyManager, account uint32, scriptHash,
|
||||||
scriptEncrypted []byte) (*scriptAddress, error) {
|
scriptEncrypted []byte) (*scriptAddress, error) {
|
||||||
|
@ -645,9 +657,131 @@ func newScriptAddress(m *ScopedKeyManager, account uint32, scriptHash,
|
||||||
}
|
}
|
||||||
|
|
||||||
return &scriptAddress{
|
return &scriptAddress{
|
||||||
manager: m,
|
baseScriptAddress: baseScriptAddress{
|
||||||
account: account,
|
manager: m,
|
||||||
address: address,
|
account: account,
|
||||||
scriptEncrypted: scriptEncrypted,
|
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
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ package waddrmgr
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -83,16 +84,17 @@ const (
|
||||||
// expectedAddr is used to house the expected return values from a managed
|
// expectedAddr is used to house the expected return values from a managed
|
||||||
// address. Not all fields for used for all managed address types.
|
// address. Not all fields for used for all managed address types.
|
||||||
type expectedAddr struct {
|
type expectedAddr struct {
|
||||||
address string
|
address string
|
||||||
addressHash []byte
|
addressHash []byte
|
||||||
internal bool
|
internal bool
|
||||||
compressed bool
|
compressed bool
|
||||||
imported bool
|
imported bool
|
||||||
pubKey []byte
|
pubKey []byte
|
||||||
privKey []byte
|
privKey []byte
|
||||||
privKeyWIF string
|
privKeyWIF string
|
||||||
script []byte
|
script []byte
|
||||||
derivationInfo DerivationPath
|
derivationInfo DerivationPath
|
||||||
|
scriptNotSecret bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// testNamePrefix is a helper to return a prefix to show for test errors based
|
// 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.
|
// the expected error when the manager is locked.
|
||||||
gotScript, err := gotAddr.Script()
|
gotScript, err := gotAddr.Script()
|
||||||
switch {
|
switch {
|
||||||
case tc.watchingOnly:
|
case tc.watchingOnly && !wantAddr.scriptNotSecret:
|
||||||
// Confirm expected watching-only error.
|
// Confirm expected watching-only error.
|
||||||
testName := fmt.Sprintf("%s Script", prefix)
|
testName := fmt.Sprintf("%s Script", prefix)
|
||||||
if !checkManagerError(tc.t, testName, err, ErrWatchingOnly) {
|
if !checkManagerError(tc.t, testName, err, ErrWatchingOnly) {
|
||||||
return false
|
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 {
|
if err != nil {
|
||||||
tc.t.Errorf("%s Script: unexpected error - got %v",
|
tc.t.Errorf("%s Script: unexpected error - got %v",
|
||||||
prefix, err)
|
prefix, err)
|
||||||
|
@ -260,6 +265,7 @@ func testManagedScriptAddress(tc *testContext, prefix string,
|
||||||
"want %x", prefix, gotScript, wantAddr.script)
|
"want %x", prefix, gotScript, wantAddr.script)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// Confirm expected locked error.
|
// Confirm expected locked error.
|
||||||
testName := fmt.Sprintf("%s Script", prefix)
|
testName := fmt.Sprintf("%s Script", prefix)
|
||||||
|
@ -883,10 +889,13 @@ func testImportPrivateKey(tc *testContext) bool {
|
||||||
// with the manager locked.
|
// with the manager locked.
|
||||||
func testImportScript(tc *testContext) bool {
|
func testImportScript(tc *testContext) bool {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
in []byte
|
in []byte
|
||||||
blockstamp BlockStamp
|
isWitness bool
|
||||||
expected expectedAddr
|
witnessVersion byte
|
||||||
|
isSecretScript bool
|
||||||
|
blockstamp BlockStamp
|
||||||
|
expected expectedAddr
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "p2sh uncompressed pubkey",
|
name: "p2sh uncompressed pubkey",
|
||||||
|
@ -924,6 +933,64 @@ func testImportScript(tc *testContext) bool {
|
||||||
// script is set to the in field during tests.
|
// 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
|
// 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 {
|
err := walletdb.Update(tc.db, func(tx walletdb.ReadWriteTx) error {
|
||||||
ns := tx.ReadWriteBucket(waddrmgrNamespaceKey)
|
ns := tx.ReadWriteBucket(waddrmgrNamespaceKey)
|
||||||
var err error
|
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
|
return err
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -979,8 +1057,28 @@ func testImportScript(tc *testContext) bool {
|
||||||
|
|
||||||
// Use the Address API to retrieve each of the expected
|
// Use the Address API to retrieve each of the expected
|
||||||
// new addresses and ensure they're accurate.
|
// new addresses and ensure they're accurate.
|
||||||
utilAddr, err := btcutil.NewAddressScriptHash(test.in,
|
var (
|
||||||
chainParams)
|
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 {
|
if err != nil {
|
||||||
tc.t.Errorf("%s NewAddressScriptHash #%d (%s): "+
|
tc.t.Errorf("%s NewAddressScriptHash #%d (%s): "+
|
||||||
"unexpected error: %v", prefix, i,
|
"unexpected error: %v", prefix, i,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package waddrmgr
|
package waddrmgr
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -789,7 +790,9 @@ func (s *ScopedKeyManager) importedAddressRowToManaged(row *dbImportedAddressRow
|
||||||
|
|
||||||
// scriptAddressRowToManaged returns a new managed address based on script
|
// scriptAddressRowToManaged returns a new managed address based on script
|
||||||
// address data loaded from the database.
|
// 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.
|
// Use the crypto public key to decrypt the imported script hash.
|
||||||
scriptHash, err := s.rootManager.cryptoKeyPub.Decrypt(row.encryptedHash)
|
scriptHash, err := s.rootManager.cryptoKeyPub.Decrypt(row.encryptedHash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -800,6 +803,24 @@ func (s *ScopedKeyManager) scriptAddressRowToManaged(row *dbScriptAddressRow) (M
|
||||||
return newScriptAddress(s, row.account, scriptHash, row.encryptedScript)
|
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
|
// rowInterfaceToManaged returns a new managed address based on the given
|
||||||
// address data loaded from the database. It will automatically select the
|
// address data loaded from the database. It will automatically select the
|
||||||
// appropriate type.
|
// appropriate type.
|
||||||
|
@ -817,6 +838,9 @@ func (s *ScopedKeyManager) rowInterfaceToManaged(ns walletdb.ReadBucket,
|
||||||
|
|
||||||
case *dbScriptAddressRow:
|
case *dbScriptAddressRow:
|
||||||
return s.scriptAddressRowToManaged(row)
|
return s.scriptAddressRowToManaged(row)
|
||||||
|
|
||||||
|
case *dbWitnessScriptAddressRow:
|
||||||
|
return s.witnessScriptAddressRowToManaged(row)
|
||||||
}
|
}
|
||||||
|
|
||||||
str := fmt.Sprintf("unsupported address type %T", rowInterface)
|
str := fmt.Sprintf("unsupported address type %T", rowInterface)
|
||||||
|
@ -2029,16 +2053,63 @@ func (s *ScopedKeyManager) toImportedPublicManagedAddress(
|
||||||
func (s *ScopedKeyManager) ImportScript(ns walletdb.ReadWriteBucket,
|
func (s *ScopedKeyManager) ImportScript(ns walletdb.ReadWriteBucket,
|
||||||
script []byte, bs *BlockStamp) (ManagedScriptAddress, error) {
|
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()
|
s.mtx.Lock()
|
||||||
defer s.mtx.Unlock()
|
defer s.mtx.Unlock()
|
||||||
|
|
||||||
// The manager must be unlocked to encrypt the imported script.
|
// 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)
|
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.
|
// Prevent duplicates.
|
||||||
scriptHash := btcutil.Hash160(script)
|
|
||||||
alreadyExists := s.existsAddress(ns, scriptHash)
|
alreadyExists := s.existsAddress(ns, scriptHash)
|
||||||
if alreadyExists {
|
if alreadyExists {
|
||||||
str := fmt.Sprintf("address for script hash %x already exists",
|
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)
|
return nil, managerError(ErrCrypto, str, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encrypt the script for storage in database using the crypto script
|
// If a key isn't considered to be "secret", we encrypt it with the
|
||||||
// key when not a watching-only address manager.
|
// public key, so we can create script addresses that also work in
|
||||||
var encryptedScript []byte
|
// watch-only mode.
|
||||||
if !s.rootManager.WatchOnly() {
|
cryptoKey := s.rootManager.cryptoKeyScript
|
||||||
encryptedScript, err = s.rootManager.cryptoKeyScript.Encrypt(
|
if !isSecretScript {
|
||||||
script,
|
cryptoKey = s.rootManager.cryptoKeyPub
|
||||||
)
|
}
|
||||||
if err != nil {
|
|
||||||
str := fmt.Sprintf("failed to encrypt script for %x",
|
// Encrypt the script for storage in database using the selected crypto
|
||||||
scriptHash)
|
// key.
|
||||||
return nil, managerError(ErrCrypto, str, err)
|
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
|
// 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
|
// Save the new imported address to the db and update start block (if
|
||||||
// needed) in a single transaction.
|
// needed) in a single transaction.
|
||||||
err = putScriptAddress(
|
switch addrType {
|
||||||
ns, &s.scope, scriptHash, ImportedAddrAccount, ssNone,
|
case WitnessScript:
|
||||||
encryptedHash, encryptedScript,
|
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 {
|
if err != nil {
|
||||||
return nil, maybeConvertDbError(err)
|
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
|
// 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
|
// since it will be cleared on lock and the script the caller passed
|
||||||
// should not be cleared out from under the caller.
|
// should not be cleared out from under the caller.
|
||||||
scriptAddr, err := newScriptAddress(
|
var (
|
||||||
s, ImportedAddrAccount, scriptHash, encryptedScript,
|
managedAddr ManagedScriptAddress
|
||||||
|
baseScriptAddr *baseScriptAddress
|
||||||
)
|
)
|
||||||
if err != nil {
|
switch addrType {
|
||||||
return nil, err
|
case WitnessScript:
|
||||||
}
|
witnessAddr, err := newWitnessScriptAddress(
|
||||||
if !s.rootManager.WatchOnly() {
|
s, ImportedAddrAccount, scriptHash, encryptedScript,
|
||||||
scriptAddr.scriptClearText = make([]byte, len(script))
|
witnessVersion, isSecretScript,
|
||||||
copy(scriptAddr.scriptClearText, script)
|
)
|
||||||
|
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
|
// Add the new managed address to the cache of recent addresses and
|
||||||
// return it.
|
// return it.
|
||||||
s.addrs[addrKey(scriptHash)] = scriptAddr
|
s.addrs[addrKey(scriptHash)] = managedAddr
|
||||||
return scriptAddr, nil
|
return managedAddr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// lookupAccount loads account number stored in the manager for the given
|
// lookupAccount loads account number stored in the manager for the given
|
||||||
|
|
Loading…
Reference in a new issue