From d0938d817f15d43384b4139a17b2fd9880866e5a Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Fri, 8 Aug 2014 15:43:50 -0500 Subject: [PATCH] Provide new wallet address manager package. This commit implements a new secure, scalable, hierarchical deterministic wallet address manager package. The following is an overview of features: - BIP0032 hierarchical deterministic keys - BIP0043/BIP0044 multi-account hierarchy - Strong focus on security: - Fully encrypted database including public information such as addresses as well as private information such as private keys and scripts needed to redeem pay-to-script-hash transactions - Hardened against memory scraping through the use of actively clearing private material from memory when locked - Different crypto keys used for public, private, and script data - Ability for different passphrases for public and private data - Scrypt-based key derivation - NaCl-based secretbox cryptography (XSalsa20 and Poly1305) - Multi-tier scalable key design to allow instant password changes regardless of the number of addresses stored - Import WIF keys - Import pay-to-script-hash scripts for things such as multi-signature transactions - Ability to export a watching-only version which does not contain any private key material - Programmatically detectable errors, including encapsulation of errors from packages it relies on - Address synchronization capabilities This commit only provides the implementation package. It does not include integration into to the existing wallet code base or conversion of existing addresses. That functionality will be provided by future commits. --- snacl/snacl.go | 238 +++++ snacl/snacl_test.go | 112 +++ waddrmgr/README.md | 62 ++ waddrmgr/address.go | 506 ++++++++++ waddrmgr/common_test.go | 72 ++ waddrmgr/cov_report.sh | 17 + waddrmgr/db.go | 1356 +++++++++++++++++++++++++++ waddrmgr/doc.go | 167 ++++ waddrmgr/error.go | 182 ++++ waddrmgr/error_test.go | 119 +++ waddrmgr/internal_test.go | 63 ++ waddrmgr/manager.go | 1821 ++++++++++++++++++++++++++++++++++++ waddrmgr/manager_test.go | 1500 +++++++++++++++++++++++++++++ waddrmgr/sync.go | 241 +++++ waddrmgr/test_coverage.txt | 126 +++ 15 files changed, 6582 insertions(+) create mode 100644 snacl/snacl.go create mode 100644 snacl/snacl_test.go create mode 100644 waddrmgr/README.md create mode 100644 waddrmgr/address.go create mode 100644 waddrmgr/common_test.go create mode 100644 waddrmgr/cov_report.sh create mode 100644 waddrmgr/db.go create mode 100644 waddrmgr/doc.go create mode 100644 waddrmgr/error.go create mode 100644 waddrmgr/error_test.go create mode 100644 waddrmgr/internal_test.go create mode 100644 waddrmgr/manager.go create mode 100644 waddrmgr/manager_test.go create mode 100644 waddrmgr/sync.go create mode 100644 waddrmgr/test_coverage.txt diff --git a/snacl/snacl.go b/snacl/snacl.go new file mode 100644 index 0000000..30da163 --- /dev/null +++ b/snacl/snacl.go @@ -0,0 +1,238 @@ +package snacl + +import ( + "crypto/rand" + "crypto/subtle" + "encoding/binary" + "errors" + "io" + + "code.google.com/p/go.crypto/nacl/secretbox" + "code.google.com/p/go.crypto/scrypt" + + "github.com/conformal/fastsha256" +) + +var ( + prng = rand.Reader +) + +var ( + ErrInvalidPassword = errors.New("invalid password") + ErrMalformed = errors.New("malformed data") + ErrDecryptFailed = errors.New("unable to decrypt") +) + +// Zero out a byte slice. +func zero(b []byte) { + for i := range b { + b[i] ^= b[i] + } +} + +const ( + KeySize = 32 + NonceSize = 24 + DefaultN = 16384 // 2^14 + DefaultR = 8 + DefaultP = 1 +) + +// CryptoKey represents a secret key which can be used to encrypt and decrypt +// data. +type CryptoKey [KeySize]byte + +// Encrypt encrypts the passed data. +func (ck *CryptoKey) Encrypt(in []byte) ([]byte, error) { + var nonce [NonceSize]byte + _, err := io.ReadFull(prng, nonce[:]) + if err != nil { + return nil, err + } + blob := secretbox.Seal(nil, in, &nonce, (*[KeySize]byte)(ck)) + return append(nonce[:], blob...), nil +} + +// Decrypt decrypts the passed data. The must be the output of the Encrypt +// function. +func (ck *CryptoKey) Decrypt(in []byte) ([]byte, error) { + if len(in) < NonceSize { + return nil, ErrMalformed + } + + var nonce [NonceSize]byte + copy(nonce[:], in[:NonceSize]) + blob := in[NonceSize:] + + opened, ok := secretbox.Open(nil, blob, &nonce, (*[KeySize]byte)(ck)) + if !ok { + return nil, ErrDecryptFailed + } + + return opened, nil +} + +// Zero clears the key by manually zeroing all memory. This is for security +// conscience application which wish to zero the memory after they've used it +// rather than waiting until it's reclaimed by the garbage collector. The +// key is no longer usable after this call. +func (ck *CryptoKey) Zero() { + zero(ck[:]) +} + +// GenerateCryptoKey generates a new crypotgraphically random key. +func GenerateCryptoKey() (*CryptoKey, error) { + var key CryptoKey + _, err := io.ReadFull(prng, key[:]) + if err != nil { + return nil, err + } + + return &key, nil +} + +// Parameters are not secret and can be stored in plain text. +type Parameters struct { + Salt [KeySize]byte + Digest [fastsha256.Size]byte + N int + R int + P int +} + +// SecretKey houses a crypto key and the parameters needed to derive it from a +// passphrase. It should only be used in memory. +type SecretKey struct { + Key *CryptoKey + Parameters Parameters +} + +// deriveKey fills out the Key field. +func (sk *SecretKey) deriveKey(password *[]byte) error { + key, err := scrypt.Key(*password, sk.Parameters.Salt[:], + sk.Parameters.N, + sk.Parameters.R, + sk.Parameters.P, + len(sk.Key)) + if err != nil { + return err + } + copy(sk.Key[:], key) + zero(key) + + return nil +} + +// Marshal returns the Parameters field marshalled into a format suitable for +// storage. This result of this can be stored in clear text. +func (sk *SecretKey) Marshal() []byte { + params := &sk.Parameters + + // The marshalled format for the the params is as follows: + //

+ // + // KeySize + fastsha256.Size + N (8 bytes) + R (8 bytes) + P (8 bytes) + marshalled := make([]byte, KeySize+fastsha256.Size+24) + + b := marshalled + copy(b[:KeySize], params.Salt[:]) + b = b[KeySize:] + copy(b[:fastsha256.Size], params.Digest[:]) + b = b[fastsha256.Size:] + binary.LittleEndian.PutUint64(b[:8], uint64(params.N)) + b = b[8:] + binary.LittleEndian.PutUint64(b[:8], uint64(params.R)) + b = b[8:] + binary.LittleEndian.PutUint64(b[:8], uint64(params.P)) + + return marshalled +} + +// Unmarshal unmarshalls the parameters needed to derive the secret key from a +// passphrase into sk. +func (sk *SecretKey) Unmarshal(marshalled []byte) error { + if sk.Key == nil { + sk.Key = (*CryptoKey)(&[KeySize]byte{}) + } + + // The marshalled format for the the params is as follows: + //

+ // + // KeySize + fastsha256.Size + N (8 bytes) + R (8 bytes) + P (8 bytes) + if len(marshalled) != KeySize+fastsha256.Size+24 { + return ErrMalformed + } + + params := &sk.Parameters + copy(params.Salt[:], marshalled[:KeySize]) + marshalled = marshalled[KeySize:] + copy(params.Digest[:], marshalled[:fastsha256.Size]) + marshalled = marshalled[fastsha256.Size:] + params.N = int(binary.LittleEndian.Uint64(marshalled[:8])) + marshalled = marshalled[8:] + params.R = int(binary.LittleEndian.Uint64(marshalled[:8])) + marshalled = marshalled[8:] + params.P = int(binary.LittleEndian.Uint64(marshalled[:8])) + + return nil +} + +// Zero zeroes the underlying secret key while leaving the parameters intact. +// This effectively makes the key unusable until it is derived again via the +// DeriveKey function. +func (sk *SecretKey) Zero() { + sk.Key.Zero() +} + +// DeriveKey derives the underlying secret key and ensures it matches the +// expected digest. This should only be called after previously calling the +// Zero function or on an initial Unmarshal. +func (sk *SecretKey) DeriveKey(password *[]byte) error { + if err := sk.deriveKey(password); err != nil { + return err + } + + // verify password + digest := fastsha256.Sum256(sk.Key[:]) + if subtle.ConstantTimeCompare(digest[:], sk.Parameters.Digest[:]) != 1 { + return ErrInvalidPassword + } + + return nil +} + +// Encrypt encrypts in bytes and returns a JSON blob. +func (sk *SecretKey) Encrypt(in []byte) ([]byte, error) { + return sk.Key.Encrypt(in) +} + +// Decrypt takes in a JSON blob and returns it's decrypted form. +func (sk *SecretKey) Decrypt(in []byte) ([]byte, error) { + return sk.Key.Decrypt(in) +} + +// NewSecretKey returns a SecretKey structure based on the passed parameters. +func NewSecretKey(password *[]byte, N, r, p int) (*SecretKey, error) { + sk := SecretKey{ + Key: (*CryptoKey)(&[KeySize]byte{}), + } + // setup parameters + sk.Parameters.N = N + sk.Parameters.R = r + sk.Parameters.P = p + _, err := io.ReadFull(prng, sk.Parameters.Salt[:]) + if err != nil { + return nil, err + } + + // derive key + err = sk.deriveKey(password) + if err != nil { + return nil, err + } + + // store digest + sk.Parameters.Digest = fastsha256.Sum256(sk.Key[:]) + + return &sk, nil +} diff --git a/snacl/snacl_test.go b/snacl/snacl_test.go new file mode 100644 index 0000000..f77a54a --- /dev/null +++ b/snacl/snacl_test.go @@ -0,0 +1,112 @@ +package snacl + +import ( + "bytes" + "testing" +) + +var ( + password = []byte("sikrit") + message = []byte("this is a secret message of sorts") + key *SecretKey + params []byte + blob []byte +) + +func TestNewSecretKey(t *testing.T) { + var err error + key, err = NewSecretKey(&password, DefaultN, DefaultR, DefaultP) + if err != nil { + t.Error(err) + return + } +} + +func TestMarshalSecretKey(t *testing.T) { + params = key.Marshal() +} + +func TestUnmarshalSecretKey(t *testing.T) { + var sk SecretKey + if err := sk.Unmarshal(params); err != nil { + t.Errorf("unexpected unmarshal error: %v", err) + return + } + + if err := sk.DeriveKey(&password); err != nil { + t.Errorf("unexpected DeriveKey error: %v", err) + return + } + + if !bytes.Equal(sk.Key[:], key.Key[:]) { + t.Errorf("keys not equal") + } +} + +func TestUnmarshalSecretKeyInvalid(t *testing.T) { + var sk SecretKey + if err := sk.Unmarshal(params); err != nil { + t.Errorf("unexpected unmarshal error: %v", err) + return + } + + p := []byte("wrong password") + if err := sk.DeriveKey(&p); err != ErrInvalidPassword { + t.Errorf("wrong password didn't fail") + return + } +} + +func TestEncrypt(t *testing.T) { + var err error + + blob, err = key.Encrypt(message) + if err != nil { + t.Error(err) + return + } +} + +func TestDecrypt(t *testing.T) { + decryptedMessage, err := key.Decrypt(blob) + if err != nil { + t.Error(err) + return + } + + if !bytes.Equal(decryptedMessage, message) { + t.Errorf("decryption failed") + return + } +} + +func TestDecryptCorrupt(t *testing.T) { + blob[len(blob)-15] = blob[len(blob)-15] + 1 + _, err := key.Decrypt(blob) + if err == nil { + t.Errorf("corrupt message decrypted") + return + } +} + +func TestZero(t *testing.T) { + var zeroKey [32]byte + + key.Zero() + if !bytes.Equal(key.Key[:], zeroKey[:]) { + t.Errorf("zero key failed") + } +} + +func TestDeriveKey(t *testing.T) { + if err := key.DeriveKey(&password); err != nil { + t.Errorf("unexpected DeriveKey key failure: %v", err) + } +} + +func TestDeriveKeyInvalid(t *testing.T) { + bogusPass := []byte("bogus") + if err := key.DeriveKey(&bogusPass); err != ErrInvalidPassword { + t.Errorf("unexpected DeriveKey key failure: %v", err) + } +} diff --git a/waddrmgr/README.md b/waddrmgr/README.md new file mode 100644 index 0000000..2b16561 --- /dev/null +++ b/waddrmgr/README.md @@ -0,0 +1,62 @@ +waddrmgr +======== + +[![Build Status](https://travis-ci.org/conformal/btcwallet.png?branch=master)] +(https://travis-ci.org/conformal/btcwallet) + +Package waddrmgr provides a secure hierarchical deterministic wallet address +manager. + +A suite of tests is provided to ensure proper functionality. See +`test_coverage.txt` for the gocov coverage report. Alternatively, if you are +running a POSIX OS, you can run the `cov_report.sh` script for a real-time +report. Package waddrmgr is licensed under the liberal ISC license. + +## Feature Overview + +- BIP0032 hierarchical deterministic keys +- BIP0043/BIP0044 multi-account hierarchy +- Strong focus on security: + - Fully encrypted database including public information such as addresses as + well as private information such as private keys and scripts needed to + redeem pay-to-script-hash transactions + - Hardened against memory scraping through the use of actively clearing + private material from memory when locked + - Different crypto keys used for public, private, and script data + - Ability for different passphrases for public and private data + - Scrypt-based key derivation + - NaCl-based secretbox cryptography (XSalsa20 and Poly1305) +- Scalable design: + - Multi-tier key design to allow instant password changes regardless of the + number of addresses stored + - Import WIF keys + - Import pay-to-script-hash scripts for things such as multi-signature + transactions + - Ability to export a watching-only version which does not contain any private + key material + - Programmatically detectable errors, including encapsulation of errors from + packages it relies on + - Address synchronization capabilities +- Comprehensive test coverage + +## Documentation + +[![GoDoc](https://godoc.org/github.com/conformal/btcwallet/waddrmgr?status.png)] +(http://godoc.org/github.com/conformal/btcwallet/waddrmgr) + +Full `go doc` style documentation for the project can be viewed online without +installing this package by using the GoDoc site here: +http://godoc.org/github.com/conformal/btcwallet/waddrmgr + +You can also view the documentation locally once the package is installed with +the `godoc` tool by running `godoc -http=":6060"` and pointing your browser to +http://localhost:6060/pkg/github.com/conformal/btcwallet/waddrmgr + +## Installation + +```bash +$ go get github.com/conformal/btcwallet/waddrmgr +``` + +Package waddrmgr is licensed under the [copyfree](http://copyfree.org) ISC +License. diff --git a/waddrmgr/address.go b/waddrmgr/address.go new file mode 100644 index 0000000..77d8b25 --- /dev/null +++ b/waddrmgr/address.go @@ -0,0 +1,506 @@ +/* + * Copyright (c) 2014 Conformal Systems LLC + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package waddrmgr + +import ( + "encoding/hex" + "fmt" + "math/big" + "sync" + + "github.com/conformal/btcec" + "github.com/conformal/btcutil" + "github.com/conformal/btcutil/hdkeychain" + "github.com/conformal/btcwallet/snacl" +) + +// zero sets all bytes in the passed slice to zero. This is used to +// explicitly clear private key material from memory. +func zero(b []byte) { + for i := range b { + b[i] ^= b[i] + } +} + +// zeroBigInt sets all bytes in the passed big int to zero and then sets the +// value to 0. This differs from simply setting the value in that it +// specifically clears the underlying bytes whereas simply setting the value +// does not. This is mostly useful to forcefully clear private keys. +func zeroBigInt(x *big.Int) { + // NOTE: This could make use of .Xor, however this is safer since the + // specific implementation of Xor could technically change in such a way + // as the original bits aren't cleared. This function would silenty + // fail in that case and it's best to avoid that possibility. + bits := x.Bits() + numBits := len(bits) + for i := 0; i < numBits; i++ { + bits[i] ^= bits[i] + } + x.SetInt64(0) +} + +// 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 +// of address. +type ManagedAddress interface { + // Account returns the account the address is associated with. + Account() uint32 + + // Address returns a btcutil.Address for the backing address. + Address() btcutil.Address + + // AddrHash returns the key or script hash related to the address + AddrHash() []byte + + // Imported returns true if the backing address was imported instead + // of being part of an address chain. + Imported() bool + + // Internal returns true if the backing address was created for internal + // use such as a change output of a transaction. + Internal() bool + + // Compressed returns true if the backing address is compressed. + Compressed() bool +} + +// ManagedPubKeyAddress extends ManagedAddress and additionally provides the +// public and private keys for pubkey-based addresses. +type ManagedPubKeyAddress interface { + ManagedAddress + + // PubKey returns the public key associated with the address. + PubKey() *btcec.PublicKey + + // ExportPubKey returns the public key associated with the address + // serialized as a hex encoded string. + ExportPubKey() string + + // PrivKey returns the private key for the address. It can fail if the + // address manager is watching-only or locked, or the address does not + // have any keys. + PrivKey() (*btcec.PrivateKey, error) + + // ExportPrivKey returns the private key associated with the address + // serialized as Wallet Import Format (WIF). + ExportPrivKey() (*btcutil.WIF, error) +} + +// ManagedScriptAddress extends ManagedAddress and represents a pay-to-script-hash +// style of bitcoin addresses. It additionally provides information about the +// script. +type ManagedScriptAddress interface { + ManagedAddress + + // Script returns the script associated with the address. + Script() ([]byte, 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 { + manager *Manager + account uint32 + address *btcutil.AddressPubKeyHash + imported bool + internal bool + compressed bool + pubKey *btcec.PublicKey + privKeyEncrypted []byte + privKeyCT []byte // non-nil if unlocked + privKeyMutex sync.Mutex +} + +// Enforce mangedAddress satisfies the ManagedPubKeyAddress interface. +var _ ManagedPubKeyAddress = (*managedAddress)(nil) + +// unlock decrypts and stores a pointer to the associated private key. It will +// fail if the key is invalid or the encrypted private key is not available. +// The returned clear text private key 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 *managedAddress) unlock(key *snacl.CryptoKey) ([]byte, error) { + // Protect concurrent access to clear text private key. + a.privKeyMutex.Lock() + defer a.privKeyMutex.Unlock() + + if len(a.privKeyCT) == 0 { + privKey, err := key.Decrypt(a.privKeyEncrypted) + if err != nil { + str := fmt.Sprintf("failed to decrypt private key for "+ + "%s", a.address) + return nil, managerError(ErrCrypto, str, err) + } + + a.privKeyCT = privKey + } + + privKeyCopy := make([]byte, len(a.privKeyCT)) + copy(privKeyCopy, a.privKeyCT) + return privKeyCopy, nil +} + +// lock zeroes the associated clear text private key. +func (a *managedAddress) lock() { + // Zero and nil the clear text private key associated with this + // address. + a.privKeyMutex.Lock() + zero(a.privKeyCT) + a.privKeyCT = nil + a.privKeyMutex.Unlock() +} + +// Account returns the account number the address is associated with. +// +// This is part of the ManagedAddress interface implementation. +func (a *managedAddress) Account() uint32 { + return a.account +} + +// Address returns the btcutil.Address which represents the managed address. +// This will be a pay-to-pubkey-hash address. +// +// This is part of the ManagedAddress interface implementation. +func (a *managedAddress) Address() btcutil.Address { + return a.address +} + +// AddrHash returns the public key hash for the address. +// +// This is part of the ManagedAddress interface implementation. +func (a *managedAddress) AddrHash() []byte { + return a.address.Hash160()[:] +} + +// Imported returns true if the address was imported instead of being part of an +// address chain. +// +// This is part of the ManagedAddress interface implementation. +func (a *managedAddress) Imported() bool { + return a.imported +} + +// Internal returns true if the address was created for internal use such as a +// change output of a transaction. +// +// This is part of the ManagedAddress interface implementation. +func (a *managedAddress) Internal() bool { + return a.internal +} + +// Compressed returns true if the address is compressed. +// +// This is part of the ManagedAddress interface implementation. +func (a *managedAddress) Compressed() bool { + return a.compressed +} + +// PubKey returns the public key associated with the address. +// +// This is part of the ManagedPubKeyAddress interface implementation. +func (a *managedAddress) PubKey() *btcec.PublicKey { + return a.pubKey +} + +// pubKeyBytes returns the serialized public key bytes for the managed address +// based on whether or not the managed address is marked as compressed. +func (a *managedAddress) pubKeyBytes() []byte { + if a.compressed { + return a.pubKey.SerializeCompressed() + } + return a.pubKey.SerializeUncompressed() +} + +// ExportPubKey returns the public key associated with the address +// serialized as a hex encoded string. +// +// This is part of the ManagedPubKeyAddress interface implementation. +func (a *managedAddress) ExportPubKey() string { + return hex.EncodeToString(a.pubKeyBytes()) +} + +// PrivKey returns the private key for the address. It can fail if the address +// manager is watching-only or locked, or the address does not have any keys. +// +// This is part of the ManagedPubKeyAddress interface implementation. +func (a *managedAddress) PrivKey() (*btcec.PrivateKey, error) { + // No private keys are available for a watching-only address manager. + if a.manager.watchingOnly { + return nil, managerError(ErrWatchingOnly, errWatchingOnly, nil) + } + + a.manager.mtx.Lock() + defer a.manager.mtx.Unlock() + + // Account manager must be unlocked to decrypt the private key. + if a.manager.locked { + return nil, managerError(ErrLocked, errLocked, nil) + } + + // Decrypt the key as needed. Also, make sure it's a copy since the + // private key stored in memory can be cleared at any time. Otherwise + // the returned private key could be invalidated from under the caller. + privKeyCopy, err := a.unlock(a.manager.cryptoKeyPriv) + if err != nil { + return nil, err + } + + privKey, _ := btcec.PrivKeyFromBytes(btcec.S256(), privKeyCopy) + zero(privKeyCopy) + return privKey, nil +} + +// ExportPrivKey returns the private key associated with the address in Wallet +// Import Format (WIF). +// +// This is part of the ManagedPubKeyAddress interface implementation. +func (a *managedAddress) ExportPrivKey() (*btcutil.WIF, error) { + pk, err := a.PrivKey() + if err != nil { + return nil, err + } + + return btcutil.NewWIF(pk, a.manager.net, a.compressed) +} + +// newManagedAddressWithoutPrivKey returns a new managed address based on the +// passed account, public key, and whether or not the public key should be +// compressed. +func newManagedAddressWithoutPrivKey(m *Manager, account uint32, pubKey *btcec.PublicKey, compressed bool) (*managedAddress, error) { + // Create a pay-to-pubkey-hash address from the public key. + var pubKeyHash []byte + if compressed { + pubKeyHash = btcutil.Hash160(pubKey.SerializeCompressed()) + } else { + pubKeyHash = btcutil.Hash160(pubKey.SerializeUncompressed()) + } + address, err := btcutil.NewAddressPubKeyHash(pubKeyHash, m.net) + if err != nil { + return nil, err + } + + return &managedAddress{ + manager: m, + address: address, + account: account, + imported: false, + internal: false, + compressed: compressed, + pubKey: pubKey, + privKeyEncrypted: nil, + privKeyCT: nil, + }, nil +} + +// newManagedAddress returns a new managed address based on the passed account, +// private key, and whether or not the public key is compressed. The managed +// address will have access to the private and public keys. +func newManagedAddress(m *Manager, account uint32, privKey *btcec.PrivateKey, compressed bool) (*managedAddress, error) { + // Encrypt the private key. + // + // NOTE: The privKeyBytes here are set into the managed address which + // are cleared when locked, so they aren't cleared here. + privKeyBytes := privKey.Serialize() + privKeyEncrypted, err := m.cryptoKeyPriv.Encrypt(privKeyBytes) + if err != nil { + str := "failed to encrypt private key" + return nil, managerError(ErrCrypto, str, err) + } + + // Leverage the code to create a managed address without a private key + // and then add the private key to it. + ecPubKey := (*btcec.PublicKey)(&privKey.PublicKey) + managedAddr, err := newManagedAddressWithoutPrivKey(m, account, + ecPubKey, compressed) + if err != nil { + return nil, err + } + managedAddr.privKeyEncrypted = privKeyEncrypted + managedAddr.privKeyCT = privKeyBytes + + return managedAddr, nil +} + +// newManagedAddressFromExtKey returns a new managed address based on the passed +// account and extended key. The managed address will have access to the +// private and public keys if the provided extended key is private, otherwise it +// will only have access to the public key. +func newManagedAddressFromExtKey(m *Manager, account uint32, key *hdkeychain.ExtendedKey) (*managedAddress, error) { + // Create a new managed address based on the public or private key + // depending on whether the generated key is private. + var managedAddr *managedAddress + if key.IsPrivate() { + privKey, err := key.ECPrivKey() + if err != nil { + return nil, err + } + + // Ensure the temp private key big integer is cleared after use. + managedAddr, err = newManagedAddress(m, account, privKey, true) + zeroBigInt(privKey.D) + if err != nil { + return nil, err + } + } else { + pubKey, err := key.ECPubKey() + if err != nil { + return nil, err + } + + managedAddr, err = newManagedAddressWithoutPrivKey(m, account, + pubKey, true) + if err != nil { + return nil, err + } + } + + return managedAddr, nil +} + +// scriptAddress represents a pay-to-script-hash address. +type scriptAddress struct { + manager *Manager + account uint32 + address *btcutil.AddressScriptHash + scriptEncrypted []byte + scriptCT []byte + 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 *snacl.CryptoKey) ([]byte, error) { + // Protect concurrent access to clear text script. + a.scriptMutex.Lock() + defer a.scriptMutex.Unlock() + + if len(a.scriptCT) == 0 { + script, err := key.Decrypt(a.scriptEncrypted) + if err != nil { + str := fmt.Sprintf("failed to decrypt script for %s", + a.address) + return nil, managerError(ErrCrypto, str, err) + } + + a.scriptCT = script + } + + scriptCopy := make([]byte, len(a.scriptCT)) + copy(scriptCopy, a.scriptCT) + return scriptCopy, nil +} + +// lock zeroes the associated clear text private key. +func (a *scriptAddress) lock() { + // Zero and nil the clear text script associated with this address. + a.scriptMutex.Lock() + zero(a.scriptCT) + a.scriptCT = nil + a.scriptMutex.Unlock() +} + +// Account returns the account the address is associated with. This will always +// be the ImportedAddrAccount constant for script addresses. +// +// This is part of the ManagedAddress interface implementation. +func (a *scriptAddress) Account() uint32 { + return a.account +} + +// Address returns the btcutil.Address which represents the managed address. +// This will be a pay-to-script-hash address. +// +// This is part of the ManagedAddress interface implementation. +func (a *scriptAddress) Address() btcutil.Address { + return a.address +} + +// AddrHash returns the script hash for the address. +// +// This is part of the ManagedAddress interface implementation. +// +// This is part of the ManagedAddress interface implementation. +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. +func (a *scriptAddress) Compressed() bool { + return false +} + +// Script returns the script associated with the address. +// +// This implements the ScriptAddress interface. +func (a *scriptAddress) Script() ([]byte, error) { + // No script is available for a watching-only address manager. + if a.manager.watchingOnly { + 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.manager.locked { + return nil, managerError(ErrLocked, errLocked, nil) + } + + // 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(a.manager.cryptoKeyScript) +} + +// newScriptAddress initializes and returns a new pay-to-script-hash address. +func newScriptAddress(m *Manager, account uint32, scriptHash, scriptEncrypted []byte) (*scriptAddress, error) { + address, err := btcutil.NewAddressScriptHashFromHash(scriptHash, m.net) + if err != nil { + return nil, err + } + + return &scriptAddress{ + manager: m, + account: account, + address: address, + scriptEncrypted: scriptEncrypted, + }, nil +} diff --git a/waddrmgr/common_test.go b/waddrmgr/common_test.go new file mode 100644 index 0000000..1e1db14 --- /dev/null +++ b/waddrmgr/common_test.go @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2014 Conformal Systems LLC + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package waddrmgr_test + +import ( + "encoding/hex" + "testing" + + "github.com/conformal/btcwallet/waddrmgr" +) + +var ( + // seed is the master seed used throughout the tests. + seed = []byte{ + 0x2a, 0x64, 0xdf, 0x08, 0x5e, 0xef, 0xed, 0xd8, 0xbf, + 0xdb, 0xb3, 0x31, 0x76, 0xb5, 0xba, 0x2e, 0x62, 0xe8, + 0xbe, 0x8b, 0x56, 0xc8, 0x83, 0x77, 0x95, 0x59, 0x8b, + 0xb6, 0xc4, 0x40, 0xc0, 0x64, + } + + pubPassphrase = []byte("_DJr{fL4H0O}*-0\n:V1izc)(6BomK") + privPassphrase = []byte("81lUHXnOMZ@?XXd7O9xyDIWIbXX-lj") + pubPassphrase2 = []byte("-0NV4P~VSJBWbunw}%/dev/null 2>&1 +if [ $? -ne 0 ]; then + echo >&2 "This script requires the gocov tool." + echo >&2 "You may obtain it with the following command:" + echo >&2 "go get github.com/axw/gocov/gocov" + exit 1 +fi +gocov test | gocov report diff --git a/waddrmgr/db.go b/waddrmgr/db.go new file mode 100644 index 0000000..c88f69d --- /dev/null +++ b/waddrmgr/db.go @@ -0,0 +1,1356 @@ +/* + * Copyright (c) 2014 Conformal Systems LLC + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package waddrmgr + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + "time" + + "github.com/conformal/bolt" + "github.com/conformal/btcwire" + "github.com/conformal/fastsha256" +) + +const ( + // LatestDbVersion is the most recent database version. + LatestDbVersion = 1 +) + +// syncStatus represents a address synchronization status stored in the +// database. +type syncStatus uint8 + +// These constants define the various supported sync status types. +// +// NOTE: These are currently unused but are being defined for the possibility of +// supporting sync status on a per-address basis. +const ( + ssNone syncStatus = 0 // not iota as they need to be stable for db + ssPartial syncStatus = 1 + ssFull syncStatus = 2 +) + +// addressType represents a type of address stored in the database. +type addressType uint8 + +// These constants define the various supported address types. +const ( + adtChain addressType = 0 // not iota as they need to be stable for db + adtImport addressType = 1 + adtScript addressType = 2 +) + +// accountType represents a type of address stored in the database. +type accountType uint8 + +// These constants define the various supported account types. +const ( + actBIP0044 accountType = 0 // not iota as they need to be stable for db +) + +// dbAccountRow houses information stored about an account in the database. +type dbAccountRow struct { + acctType accountType + rawData []byte // Varies based on account type field. +} + +// dbBIP0044AccountRow houses additional information stored about a BIP0044 +// account in the database. +type dbBIP0044AccountRow struct { + dbAccountRow + pubKeyEncrypted []byte + privKeyEncrypted []byte + nextExternalIndex uint32 + nextInternalIndex uint32 + name string +} + +// dbAddressRow houses common information stored about an address in the +// database. +type dbAddressRow struct { + addrType addressType + account uint32 + addTime uint64 + syncStatus syncStatus + rawData []byte // Varies based on address type field. +} + +// dbChainAddressRow houses additional information stored about a chained +// address in the database. +type dbChainAddressRow struct { + dbAddressRow + branch uint32 + index uint32 +} + +// dbImportedAddressRow houses additional information stored about an imported +// public key address in the database. +type dbImportedAddressRow struct { + dbAddressRow + encryptedPubKey []byte + encryptedPrivKey []byte +} + +// dbImportedAddressRow houses additional information stored about a script +// address in the database. +type dbScriptAddressRow struct { + dbAddressRow + encryptedHash []byte + encryptedScript []byte +} + +// Key names for various database fields. +var ( + // Bucket names. + acctBucketName = []byte("acct") + addrBucketName = []byte("addr") + addrAcctIdxBucketName = []byte("addracctidx") + mainBucketName = []byte("main") + syncBucketName = []byte("sync") + + // Db related key names (main bucket). + dbVersionName = []byte("dbver") + dbCreateDateName = []byte("dbcreated") + + // Crypto related key names (main bucket). + masterPrivKeyName = []byte("mpriv") + masterPubKeyName = []byte("mpub") + cryptoPrivKeyName = []byte("cpriv") + cryptoPubKeyName = []byte("cpub") + cryptoScriptKeyName = []byte("cscript") + watchingOnlyName = []byte("watchonly") + + // Sync related key names (sync bucket). + syncedToName = []byte("syncedto") + startBlockName = []byte("startblock") + recentBlocksName = []byte("recentblocks") + + // Account related key names (account bucket). + acctNumAcctsName = []byte("numaccts") +) + +// managerTx represents a database transaction on which all database reads and +// writes occur. Note that fetched bytes are only valid during the bolt +// transaction, however they are safe to use after a manager transation has +// been terminated. This is why the code make copies of the data fetched from +// bolt buckets. +type managerTx bolt.Tx + +// FetchMasterKeyParams loads the master key parameters needed to derive them +// (when given the correct user-supplied passphrase) from the database. Either +// returned value can be nil, but in practice only the private key params will +// be nil for a watching-only database. +func (mtx *managerTx) FetchMasterKeyParams() ([]byte, []byte, error) { + bucket := (*bolt.Tx)(mtx).Bucket(mainBucketName) + + // Load the master public key parameters. Required. + val := bucket.Get(masterPubKeyName) + if val == nil { + str := "required master public key parameters not stored in " + + "database" + return nil, nil, managerError(ErrDatabase, str, nil) + } + pubParams := make([]byte, len(val)) + copy(pubParams, val) + + // Load the master private key parameters if they were stored. + var privParams []byte + val = bucket.Get(masterPrivKeyName) + if val != nil { + privParams = make([]byte, len(val)) + copy(privParams, val) + } + + return pubParams, privParams, nil +} + +// PutMasterKeyParams stores the master key parameters needed to derive them +// to the database. Either parameter can be nil in which case no value is +// written for the parameter. +func (mtx *managerTx) PutMasterKeyParams(pubParams, privParams []byte) error { + bucket := (*bolt.Tx)(mtx).Bucket(mainBucketName) + + if privParams != nil { + err := bucket.Put(masterPrivKeyName, privParams) + if err != nil { + str := "failed to store master private key parameters" + return managerError(ErrDatabase, str, err) + } + } + + if pubParams != nil { + err := bucket.Put(masterPubKeyName, pubParams) + if err != nil { + str := "failed to store master public key parameters" + return managerError(ErrDatabase, str, err) + } + } + + return nil +} + +// FetchCryptoKeys loads the encrypted crypto keys which are in turn used to +// protect the extended keys, imported keys, and scripts. Any of the returned +// values can be nil, but in practice only the crypto private and script keys +// will be nil for a watching-only database. +func (mtx *managerTx) FetchCryptoKeys() ([]byte, []byte, []byte, error) { + bucket := (*bolt.Tx)(mtx).Bucket(mainBucketName) + + // Load the crypto public key parameters. Required. + val := bucket.Get(cryptoPubKeyName) + if val == nil { + str := "required encrypted crypto public not stored in database" + return nil, nil, nil, managerError(ErrDatabase, str, nil) + } + pubKey := make([]byte, len(val)) + copy(pubKey, val) + + // Load the crypto private key parameters if they were stored. + var privKey []byte + val = bucket.Get(cryptoPrivKeyName) + if val != nil { + privKey = make([]byte, len(val)) + copy(privKey, val) + } + + // Load the crypto script key parameters if they were stored. + var scriptKey []byte + val = bucket.Get(cryptoScriptKeyName) + if val != nil { + scriptKey = make([]byte, len(val)) + copy(scriptKey, val) + } + + return pubKey, privKey, scriptKey, nil +} + +// PutCryptoKeys stores the encrypted crypto keys which are in turn used to +// protect the extended and imported keys. Either parameter can be nil in which +// case no value is written for the parameter. +func (mtx *managerTx) PutCryptoKeys(pubKeyEncrypted, privKeyEncrypted, scriptKeyEncrypted []byte) error { + bucket := (*bolt.Tx)(mtx).Bucket(mainBucketName) + + if pubKeyEncrypted != nil { + err := bucket.Put(cryptoPubKeyName, pubKeyEncrypted) + if err != nil { + str := "failed to store encrypted crypto public key" + return managerError(ErrDatabase, str, err) + } + } + + if privKeyEncrypted != nil { + err := bucket.Put(cryptoPrivKeyName, privKeyEncrypted) + if err != nil { + str := "failed to store encrypted crypto private key" + return managerError(ErrDatabase, str, err) + } + } + + if scriptKeyEncrypted != nil { + err := bucket.Put(cryptoScriptKeyName, scriptKeyEncrypted) + if err != nil { + str := "failed to store encrypted crypto script key" + return managerError(ErrDatabase, str, err) + } + } + + return nil +} + +// FetchWatchingOnly loads the watching-only flag from the database. +func (mtx *managerTx) FetchWatchingOnly() (bool, error) { + bucket := (*bolt.Tx)(mtx).Bucket(mainBucketName) + buf := bucket.Get(watchingOnlyName) + if len(buf) != 1 { + str := "malformed watching-only flag stored in database" + return false, managerError(ErrDatabase, str, nil) + } + + return buf[0] != 0, nil +} + +// PutWatchingOnly stores the watching-only flag to the database. +func (mtx *managerTx) PutWatchingOnly(watchingOnly bool) error { + bucket := (*bolt.Tx)(mtx).Bucket(mainBucketName) + var encoded byte + if watchingOnly { + encoded = 1 + } + + if err := bucket.Put(watchingOnlyName, []byte{encoded}); err != nil { + str := "failed to store wathcing only flag" + return managerError(ErrDatabase, str, err) + } + return nil +} + +// accountKey returns the account key to use in the database for a given account +// number. +func accountKey(account uint32) []byte { + buf := make([]byte, 4) + binary.LittleEndian.PutUint32(buf, account) + return buf +} + +// deserializeAccountRow deserializes the passed serialized account information. +// This is used as a common base for the various account types to deserialize +// the common parts. +func deserializeAccountRow(accountID []byte, serializedAccount []byte) (*dbAccountRow, error) { + // The serialized account format is: + // + // + // 1 byte acctType + 4 bytes raw data length + raw data + + // Given the above, the length of the entry must be at a minimum + // the constant value sizes. + if len(serializedAccount) < 5 { + str := fmt.Sprintf("malformed serialized account for key %x", + accountID) + return nil, managerError(ErrDatabase, str, nil) + } + + row := dbAccountRow{} + row.acctType = accountType(serializedAccount[0]) + rdlen := binary.LittleEndian.Uint32(serializedAccount[1:5]) + row.rawData = make([]byte, rdlen) + copy(row.rawData, serializedAccount[5:5+rdlen]) + + return &row, nil +} + +// serializeAccountRow returns the serialization of the passed account row. +func serializeAccountRow(row *dbAccountRow) []byte { + // The serialized account format is: + // + // + // 1 byte acctType + 4 bytes raw data length + raw data + rdlen := len(row.rawData) + buf := make([]byte, 5+rdlen) + buf[0] = byte(row.acctType) + binary.LittleEndian.PutUint32(buf[1:5], uint32(rdlen)) + copy(buf[5:5+rdlen], row.rawData) + return buf +} + +// deserializeBIP0044AccountRow deserializes the raw data from the passed +// account row as a BIP0044 account. +func deserializeBIP0044AccountRow(accountID []byte, row *dbAccountRow) (*dbBIP0044AccountRow, error) { + // The serialized BIP0044 account raw data format is: + // + // + // + // 4 bytes encrypted pubkey len + encrypted pubkey + 4 bytes encrypted + // privkey len + encrypted privkey + 4 bytes next external index + + // 4 bytes next internal index + 4 bytes name len + name + + // Given the above, the length of the entry must be at a minimum + // the constant value sizes. + if len(row.rawData) < 20 { + str := fmt.Sprintf("malformed serialized bip0044 account for "+ + "key %x", accountID) + return nil, managerError(ErrDatabase, str, nil) + } + + retRow := dbBIP0044AccountRow{ + dbAccountRow: *row, + } + + pubLen := binary.LittleEndian.Uint32(row.rawData[0:4]) + retRow.pubKeyEncrypted = make([]byte, pubLen) + copy(retRow.pubKeyEncrypted, row.rawData[4:4+pubLen]) + offset := 4 + pubLen + privLen := binary.LittleEndian.Uint32(row.rawData[offset : offset+4]) + offset += 4 + retRow.privKeyEncrypted = make([]byte, privLen) + copy(retRow.privKeyEncrypted, row.rawData[offset:offset+privLen]) + offset += privLen + retRow.nextExternalIndex = binary.LittleEndian.Uint32(row.rawData[offset : offset+4]) + offset += 4 + retRow.nextInternalIndex = binary.LittleEndian.Uint32(row.rawData[offset : offset+4]) + offset += 4 + nameLen := binary.LittleEndian.Uint32(row.rawData[offset : offset+4]) + offset += 4 + retRow.name = string(row.rawData[offset : offset+nameLen]) + + return &retRow, nil +} + +// serializeBIP0044AccountRow returns the serialization of the raw data field +// for a BIP0044 account. +func serializeBIP0044AccountRow(encryptedPubKey, + encryptedPrivKey []byte, nextExternalIndex, nextInternalIndex uint32, + name string) []byte { + + // The serialized BIP0044 account raw data format is: + // + // + // + // 4 bytes encrypted pubkey len + encrypted pubkey + 4 bytes encrypted + // privkey len + encrypted privkey + 4 bytes next external index + + // 4 bytes next internal index + 4 bytes name len + name + pubLen := uint32(len(encryptedPubKey)) + privLen := uint32(len(encryptedPrivKey)) + nameLen := uint32(len(name)) + rawData := make([]byte, 20+pubLen+privLen+nameLen) + binary.LittleEndian.PutUint32(rawData[0:4], pubLen) + copy(rawData[4:4+pubLen], encryptedPubKey) + offset := 4 + pubLen + binary.LittleEndian.PutUint32(rawData[offset:offset+4], privLen) + offset += 4 + copy(rawData[offset:offset+privLen], encryptedPrivKey) + offset += privLen + binary.LittleEndian.PutUint32(rawData[offset:offset+4], nextExternalIndex) + offset += 4 + binary.LittleEndian.PutUint32(rawData[offset:offset+4], nextInternalIndex) + offset += 4 + binary.LittleEndian.PutUint32(rawData[offset:offset+4], nameLen) + offset += 4 + copy(rawData[offset:offset+nameLen], name) + return rawData +} + +// FetchAccountInfo loads information about the passed account from the +// database. +func (mtx *managerTx) FetchAccountInfo(account uint32) (interface{}, error) { + bucket := (*bolt.Tx)(mtx).Bucket(acctBucketName) + + accountID := accountKey(account) + serializedRow := bucket.Get(accountID) + if serializedRow == nil { + str := fmt.Sprintf("account %d not found", account) + return nil, managerError(ErrAccountNotFound, str, nil) + } + + row, err := deserializeAccountRow(accountID, serializedRow) + if err != nil { + return nil, err + } + + switch row.acctType { + case actBIP0044: + return deserializeBIP0044AccountRow(accountID, row) + } + + str := fmt.Sprintf("unsupported account type '%d'", row.acctType) + return nil, managerError(ErrDatabase, str, nil) +} + +// putAccountRow stores the provided account information to the database. This +// is used a common base for storing the various account types. +func (mtx *managerTx) putAccountRow(account uint32, row *dbAccountRow) error { + bucket := (*bolt.Tx)(mtx).Bucket(acctBucketName) + + // Write the serialized value keyed by the account number. + err := bucket.Put(accountKey(account), serializeAccountRow(row)) + if err != nil { + str := fmt.Sprintf("failed to store account %d", account) + return managerError(ErrDatabase, str, err) + } + return nil +} + +// PutAccountInfo stores the provided account information to the database. +func (mtx *managerTx) PutAccountInfo(account uint32, encryptedPubKey, + encryptedPrivKey []byte, nextExternalIndex, nextInternalIndex uint32, + name string) error { + + rawData := serializeBIP0044AccountRow(encryptedPubKey, encryptedPrivKey, + nextExternalIndex, nextInternalIndex, name) + + acctRow := dbAccountRow{ + acctType: actBIP0044, + rawData: rawData, + } + return mtx.putAccountRow(account, &acctRow) +} + +// FetchNumAccounts loads the number of accounts that have been created from +// the database. +func (mtx *managerTx) FetchNumAccounts() (uint32, error) { + bucket := (*bolt.Tx)(mtx).Bucket(acctBucketName) + + val := bucket.Get(acctNumAcctsName) + if val == nil { + str := "required num accounts not stored in database" + return 0, managerError(ErrDatabase, str, nil) + } + + return binary.LittleEndian.Uint32(val), nil +} + +// PutNumAccounts stores the number of accounts that have been created to the +// database. +func (mtx *managerTx) PutNumAccounts(numAccounts uint32) error { + bucket := (*bolt.Tx)(mtx).Bucket(acctBucketName) + + var val [4]byte + binary.LittleEndian.PutUint32(val[:], numAccounts) + err := bucket.Put(acctNumAcctsName, val[:]) + if err != nil { + str := "failed to store num accounts" + return managerError(ErrDatabase, str, err) + } + + return nil +} + +// fetchAddressRow loads address information for the provided address id from +// the database. This is used as a common base for the various address types +// to load the common information. + +// deserializeAddressRow deserializes the passed serialized address information. +// This is used as a common base for the various address types to deserialize +// the common parts. +func deserializeAddressRow(addressID, serializedAddress []byte) (*dbAddressRow, error) { + // The serialized address format is: + // + // + // 1 byte addrType + 4 bytes account + 8 bytes addTime + 1 byte + // syncStatus + 4 bytes raw data length + raw data + + // Given the above, the length of the entry must be at a minimum + // the constant value sizes. + if len(serializedAddress) < 18 { + str := fmt.Sprintf("malformed serialized address for key %s", + addressID) + return nil, managerError(ErrDatabase, str, nil) + } + + row := dbAddressRow{} + row.addrType = addressType(serializedAddress[0]) + row.account = binary.LittleEndian.Uint32(serializedAddress[1:5]) + row.addTime = binary.LittleEndian.Uint64(serializedAddress[5:13]) + row.syncStatus = syncStatus(serializedAddress[13]) + rdlen := binary.LittleEndian.Uint32(serializedAddress[14:18]) + row.rawData = make([]byte, rdlen) + copy(row.rawData, serializedAddress[18:18+rdlen]) + + return &row, nil +} + +// serializeAddressRow returns the serialization of the passed address row. +func serializeAddressRow(row *dbAddressRow) []byte { + // The serialized address format is: + // + // + // + // 1 byte addrType + 4 bytes account + 8 bytes addTime + 1 byte + // syncStatus + 4 bytes raw data length + raw data + rdlen := len(row.rawData) + buf := make([]byte, 18+rdlen) + buf[0] = byte(row.addrType) + binary.LittleEndian.PutUint32(buf[1:5], row.account) + binary.LittleEndian.PutUint64(buf[5:13], row.addTime) + buf[13] = byte(row.syncStatus) + binary.LittleEndian.PutUint32(buf[14:18], uint32(rdlen)) + copy(buf[18:18+rdlen], row.rawData) + return buf +} + +// deserializeChainedAddress deserializes the raw data from the passed address +// row as a chained address. +func deserializeChainedAddress(addressID []byte, row *dbAddressRow) (*dbChainAddressRow, error) { + // The serialized chain address raw data format is: + // + // + // 4 bytes branch + 4 bytes address index + if len(row.rawData) != 8 { + str := fmt.Sprintf("malformed serialized chained address for "+ + "key %s", addressID) + return nil, managerError(ErrDatabase, str, nil) + } + + retRow := dbChainAddressRow{ + dbAddressRow: *row, + } + + retRow.branch = binary.LittleEndian.Uint32(row.rawData[0:4]) + retRow.index = binary.LittleEndian.Uint32(row.rawData[4:8]) + + return &retRow, nil +} + +// serializeChainedAddress returns the serialization of the raw data field for +// a chained address. +func serializeChainedAddress(branch, index uint32) []byte { + // The serialized chain address raw data format is: + // + // + // 4 bytes branch + 4 bytes address index + rawData := make([]byte, 8) + binary.LittleEndian.PutUint32(rawData[0:4], branch) + binary.LittleEndian.PutUint32(rawData[4:8], index) + return rawData +} + +// deserializeImportedAddress deserializes the raw data from the passed address +// row as an imported address. +func deserializeImportedAddress(addressID []byte, row *dbAddressRow) (*dbImportedAddressRow, error) { + // The serialized imported address raw data format is: + // + // + // 4 bytes encrypted pubkey len + encrypted pubkey + 4 bytes encrypted + // privkey len + encrypted privkey + + // Given the above, the length of the entry must be at a minimum + // the constant value sizes. + if len(row.rawData) < 8 { + str := fmt.Sprintf("malformed serialized imported address for "+ + "key %s", addressID) + return nil, managerError(ErrDatabase, str, nil) + } + + retRow := dbImportedAddressRow{ + dbAddressRow: *row, + } + + pubLen := binary.LittleEndian.Uint32(row.rawData[0:4]) + retRow.encryptedPubKey = make([]byte, pubLen) + copy(retRow.encryptedPubKey, row.rawData[4:4+pubLen]) + offset := 4 + pubLen + privLen := binary.LittleEndian.Uint32(row.rawData[offset : offset+4]) + offset += 4 + retRow.encryptedPrivKey = make([]byte, privLen) + copy(retRow.encryptedPrivKey, row.rawData[offset:offset+privLen]) + + return &retRow, nil +} + +// serializeImportedAddress returns the serialization of the raw data field for +// an imported address. +func serializeImportedAddress(encryptedPubKey, encryptedPrivKey []byte) []byte { + // The serialized imported address raw data format is: + // + // + // 4 bytes encrypted pubkey len + encrypted pubkey + 4 bytes encrypted + // privkey len + encrypted privkey + pubLen := uint32(len(encryptedPubKey)) + privLen := uint32(len(encryptedPrivKey)) + rawData := make([]byte, 8+pubLen+privLen) + binary.LittleEndian.PutUint32(rawData[0:4], pubLen) + copy(rawData[4:4+pubLen], encryptedPubKey) + offset := 4 + pubLen + binary.LittleEndian.PutUint32(rawData[offset:offset+4], privLen) + offset += 4 + copy(rawData[offset:offset+privLen], encryptedPrivKey) + return rawData +} + +// deserializeScriptAddress deserializes the raw data from the passed address +// row as a script address. +func deserializeScriptAddress(addressID []byte, row *dbAddressRow) (*dbScriptAddressRow, error) { + // The serialized script address raw data format is: + // + // + // 4 bytes encrypted script hash len + encrypted script hash + 4 bytes + // encrypted script len + encrypted script + + // Given the above, the length of the entry must be at a minimum + // the constant value sizes. + if len(row.rawData) < 8 { + str := fmt.Sprintf("malformed serialized script address for "+ + "key %s", addressID) + return nil, managerError(ErrDatabase, str, nil) + } + + retRow := dbScriptAddressRow{ + dbAddressRow: *row, + } + + hashLen := binary.LittleEndian.Uint32(row.rawData[0:4]) + retRow.encryptedHash = make([]byte, hashLen) + copy(retRow.encryptedHash, row.rawData[4:4+hashLen]) + offset := 4 + hashLen + scriptLen := binary.LittleEndian.Uint32(row.rawData[offset : offset+4]) + offset += 4 + retRow.encryptedScript = make([]byte, scriptLen) + copy(retRow.encryptedScript, row.rawData[offset:offset+scriptLen]) + + return &retRow, nil +} + +// serializeScriptAddress returns the serialization of the raw data field for +// a script address. +func serializeScriptAddress(encryptedHash, encryptedScript []byte) []byte { + // The serialized script address raw data format is: + // + // + // 4 bytes encrypted script hash len + encrypted script hash + 4 bytes + // encrypted script len + encrypted script + + hashLen := uint32(len(encryptedHash)) + scriptLen := uint32(len(encryptedScript)) + rawData := make([]byte, 8+hashLen+scriptLen) + binary.LittleEndian.PutUint32(rawData[0:4], hashLen) + copy(rawData[4:4+hashLen], encryptedHash) + offset := 4 + hashLen + binary.LittleEndian.PutUint32(rawData[offset:offset+4], scriptLen) + offset += 4 + copy(rawData[offset:offset+scriptLen], encryptedScript) + return rawData +} + +// FetchAddress loads address information for the provided address id from +// the database. The returned value is one of the address rows for the specific +// address type. The caller should use type assertions to ascertain the type. +func (mtx *managerTx) FetchAddress(addressID []byte) (interface{}, error) { + bucket := (*bolt.Tx)(mtx).Bucket(addrBucketName) + + addrHash := fastsha256.Sum256(addressID) + serializedRow := bucket.Get(addrHash[:]) + if serializedRow == nil { + str := "address not found" + return nil, managerError(ErrAddressNotFound, str, nil) + } + + row, err := deserializeAddressRow(addressID, serializedRow) + if err != nil { + return nil, err + } + + switch row.addrType { + case adtChain: + return deserializeChainedAddress(addressID, row) + case adtImport: + return deserializeImportedAddress(addressID, row) + case adtScript: + return deserializeScriptAddress(addressID, row) + } + + str := fmt.Sprintf("unsupported address type '%d'", row.addrType) + return nil, managerError(ErrDatabase, str, nil) +} + +// putAddress stores the provided address information to the database. This +// is used a common base for storing the various address types. +func (mtx *managerTx) putAddress(addressID []byte, row *dbAddressRow) error { + bucket := (*bolt.Tx)(mtx).Bucket(addrBucketName) + + // Write the serialized value keyed by the hash of the address. The + // additional hash is used to conceal the actual address while still + // allowed keyed lookups. + addrHash := fastsha256.Sum256(addressID) + err := bucket.Put(addrHash[:], serializeAddressRow(row)) + if err != nil { + str := fmt.Sprintf("failed to store address %x", addressID) + return managerError(ErrDatabase, str, err) + } + + return nil +} + +// PutChainedAddress stores the provided chained address information to the +// database. +func (mtx *managerTx) PutChainedAddress(addressID []byte, account uint32, + status syncStatus, branch, index uint32) error { + + addrRow := dbAddressRow{ + addrType: adtChain, + account: account, + addTime: uint64(time.Now().Unix()), + syncStatus: status, + rawData: serializeChainedAddress(branch, index), + } + if err := mtx.putAddress(addressID, &addrRow); err != nil { + return err + } + + // Update the next index for the appropriate internal or external + // branch. + accountID := accountKey(account) + bucket := (*bolt.Tx)(mtx).Bucket(acctBucketName) + serializedAccount := bucket.Get(accountID) + + // Deserialize the account row. + row, err := deserializeAccountRow(accountID, serializedAccount) + if err != nil { + return err + } + arow, err := deserializeBIP0044AccountRow(accountID, row) + if err != nil { + return err + } + + // Increment the appropriate next index depending on whether the branch + // is internal or external. + nextExternalIndex := arow.nextExternalIndex + nextInternalIndex := arow.nextInternalIndex + if branch == internalBranch { + nextInternalIndex = index + 1 + } else { + nextExternalIndex = index + 1 + } + + // Reserialize the account with the updated index and store it. + row.rawData = serializeBIP0044AccountRow(arow.pubKeyEncrypted, + arow.privKeyEncrypted, nextExternalIndex, nextInternalIndex, + arow.name) + err = bucket.Put(accountID, serializeAccountRow(row)) + if err != nil { + str := fmt.Sprintf("failed to update next index for "+ + "address %x, account %d", addressID, account) + return managerError(ErrDatabase, str, err) + } + return nil +} + +// PutImportedAddress stores the provided imported address information to the +// database. +func (mtx *managerTx) PutImportedAddress(addressID []byte, account uint32, + status syncStatus, encryptedPubKey, encryptedPrivKey []byte) error { + + rawData := serializeImportedAddress(encryptedPubKey, encryptedPrivKey) + addrRow := dbAddressRow{ + addrType: adtImport, + account: account, + addTime: uint64(time.Now().Unix()), + syncStatus: status, + rawData: rawData, + } + return mtx.putAddress(addressID, &addrRow) +} + +// PutScriptAddress stores the provided script address information to the +// database. +func (mtx *managerTx) PutScriptAddress(addressID []byte, account uint32, + status syncStatus, encryptedHash, encryptedScript []byte) error { + + rawData := serializeScriptAddress(encryptedHash, encryptedScript) + addrRow := dbAddressRow{ + addrType: adtScript, + account: account, + addTime: uint64(time.Now().Unix()), + syncStatus: status, + rawData: rawData, + } + if err := mtx.putAddress(addressID, &addrRow); err != nil { + return err + } + + return nil +} + +// ExistsAddress returns whether or not the address id exists in the database. +func (mtx *managerTx) ExistsAddress(addressID []byte) bool { + bucket := (*bolt.Tx)(mtx).Bucket(addrBucketName) + + addrHash := fastsha256.Sum256(addressID) + return bucket.Get(addrHash[:]) != nil +} + +// FetchAllAddresses loads information about all addresses from the database. +// The returned value is a slice of address rows for each specific address type. +// The caller should use type assertions to ascertain the types. +func (mtx *managerTx) FetchAllAddresses() ([]interface{}, error) { + bucket := (*bolt.Tx)(mtx).Bucket(addrBucketName) + + var addrs []interface{} + cursor := bucket.Cursor() + for k, v := cursor.First(); k != nil; k, v = cursor.Next() { + // Skip buckets. + if v == nil { + continue + } + + // Deserialize the address row first to determine the field + // values. + row, err := deserializeAddressRow(k, v) + if err != nil { + return nil, err + } + + var addrRow interface{} + switch row.addrType { + case adtChain: + addrRow, err = deserializeChainedAddress(k, row) + case adtImport: + addrRow, err = deserializeImportedAddress(k, row) + case adtScript: + addrRow, err = deserializeScriptAddress(k, row) + default: + str := fmt.Sprintf("unsupported address type '%d'", + row.addrType) + return nil, managerError(ErrDatabase, str, nil) + } + if err != nil { + return nil, err + } + + addrs = append(addrs, addrRow) + } + + return addrs, nil +} + +// DeletePrivateKeys removes all private key material from the database. +// +// NOTE: Care should be taken when calling this function. It is primarily +// intended for use in converting to a watching-only copy. Removing the private +// keys from the main database without also marking it watching-only will result +// in an unusable database. It will also make any imported scripts and private +// keys unrecoverable unless there is a backup copy available. +func (mtx *managerTx) DeletePrivateKeys() error { + bucket := (*bolt.Tx)(mtx).Bucket(mainBucketName) + + // Delete the master private key params and the crypto private and + // script keys. + if err := bucket.Delete(masterPrivKeyName); err != nil { + str := "failed to delete master private key parameters" + return managerError(ErrDatabase, str, err) + } + if err := bucket.Delete(cryptoPrivKeyName); err != nil { + str := "failed to delete crypto private key" + return managerError(ErrDatabase, str, err) + } + if err := bucket.Delete(cryptoScriptKeyName); err != nil { + str := "failed to delete crypto script key" + return managerError(ErrDatabase, str, err) + } + + // Delete the account extended private key for all accounts. + bucket = (*bolt.Tx)(mtx).Bucket(acctBucketName) + cursor := bucket.Cursor() + for k, v := cursor.First(); k != nil; k, v = cursor.Next() { + // Skip buckets. + if v == nil || bytes.Equal(k, acctNumAcctsName) { + continue + } + + // Deserialize the account row first to determine the type. + row, err := deserializeAccountRow(k, v) + if err != nil { + return err + } + + switch row.acctType { + case actBIP0044: + arow, err := deserializeBIP0044AccountRow(k, row) + if err != nil { + return err + } + + // Reserialize the account without the private key and + // store it. + row.rawData = serializeBIP0044AccountRow( + arow.pubKeyEncrypted, nil, + arow.nextExternalIndex, arow.nextInternalIndex, + arow.name) + err = bucket.Put(k, serializeAccountRow(row)) + if err != nil { + str := "failed to delete account private key" + return managerError(ErrDatabase, str, err) + } + } + } + + // Delete the private key for all imported addresses. + bucket = (*bolt.Tx)(mtx).Bucket(addrBucketName) + cursor = bucket.Cursor() + for k, v := cursor.First(); k != nil; k, v = cursor.Next() { + // Skip buckets. + if v == nil { + continue + } + + // Deserialize the address row first to determine the field + // values. + row, err := deserializeAddressRow(k, v) + if err != nil { + return err + } + + switch row.addrType { + case adtImport: + irow, err := deserializeImportedAddress(k, row) + if err != nil { + return err + } + + // Reserialize the imported address without the private + // key and store it. + row.rawData = serializeImportedAddress( + irow.encryptedPubKey, nil) + err = bucket.Put(k, serializeAddressRow(row)) + if err != nil { + str := "failed to delete imported private key" + return managerError(ErrDatabase, str, err) + } + + case adtScript: + srow, err := deserializeScriptAddress(k, row) + if err != nil { + return err + } + + // Reserialize the script address without the script + // and store it. + row.rawData = serializeScriptAddress(srow.encryptedHash, + nil) + err = bucket.Put(k, serializeAddressRow(row)) + if err != nil { + str := "failed to delete imported script" + return managerError(ErrDatabase, str, err) + } + } + } + + return nil +} + +// FetchSyncedTo loads the block stamp the manager is synced to from the +// database. +func (mtx *managerTx) FetchSyncedTo() (*BlockStamp, error) { + bucket := (*bolt.Tx)(mtx).Bucket(syncBucketName) + + // The serialized synced to format is: + // + // + // 4 bytes block height + 32 bytes hash length + buf := bucket.Get(syncedToName) + if len(buf) != 36 { + str := "malformed sync information stored in database" + return nil, managerError(ErrDatabase, str, nil) + } + + var bs BlockStamp + bs.Height = int32(binary.LittleEndian.Uint32(buf[0:4])) + copy(bs.Hash[:], buf[4:36]) + return &bs, nil +} + +// PutSyncedTo stores the provided synced to blockstamp to the database. +func (mtx *managerTx) PutSyncedTo(bs *BlockStamp) error { + bucket := (*bolt.Tx)(mtx).Bucket(syncBucketName) + + // The serialized synced to format is: + // + // + // 4 bytes block height + 32 bytes hash length + buf := make([]byte, 36) + binary.LittleEndian.PutUint32(buf[0:4], uint32(bs.Height)) + copy(buf[4:36], bs.Hash[0:32]) + + err := bucket.Put(syncedToName, buf) + if err != nil { + str := fmt.Sprintf("failed to store sync information %v", bs.Hash) + return managerError(ErrDatabase, str, err) + } + return nil +} + +// FetchStartBlock loads the start block stamp for the manager from the +// database. +func (mtx *managerTx) FetchStartBlock() (*BlockStamp, error) { + bucket := (*bolt.Tx)(mtx).Bucket(syncBucketName) + + // The serialized start block format is: + // + // + // 4 bytes block height + 32 bytes hash length + buf := bucket.Get(startBlockName) + if len(buf) != 36 { + str := "malformed start block stored in database" + return nil, managerError(ErrDatabase, str, nil) + } + + var bs BlockStamp + bs.Height = int32(binary.LittleEndian.Uint32(buf[0:4])) + copy(bs.Hash[:], buf[4:36]) + return &bs, nil +} + +// PutStartBlock stores the provided start block stamp to the database. +func (mtx *managerTx) PutStartBlock(bs *BlockStamp) error { + bucket := (*bolt.Tx)(mtx).Bucket(syncBucketName) + + // The serialized start block format is: + // + // + // 4 bytes block height + 32 bytes hash length + buf := make([]byte, 36) + binary.LittleEndian.PutUint32(buf[0:4], uint32(bs.Height)) + copy(buf[4:36], bs.Hash[0:32]) + + err := bucket.Put(startBlockName, buf) + if err != nil { + str := fmt.Sprintf("failed to store start block %v", bs.Hash) + return managerError(ErrDatabase, str, err) + } + return nil +} + +// FetchRecentBlocks returns the height of the most recent block height and +// hashes of the most recent blocks. +func (mtx *managerTx) FetchRecentBlocks() (int32, []btcwire.ShaHash, error) { + bucket := (*bolt.Tx)(mtx).Bucket(syncBucketName) + + // The serialized recent blocks format is: + // + // + // 4 bytes recent block height + 4 bytes number of hashes + raw hashes + // at 32 bytes each. + + // Given the above, the length of the entry must be at a minimum + // the constant value sizes. + buf := bucket.Get(recentBlocksName) + if len(buf) < 8 { + str := "malformed recent blocks stored in database" + return 0, nil, managerError(ErrDatabase, str, nil) + } + + recentHeight := int32(binary.LittleEndian.Uint32(buf[0:4])) + numHashes := binary.LittleEndian.Uint32(buf[4:8]) + recentHashes := make([]btcwire.ShaHash, numHashes) + offset := 8 + for i := uint32(0); i < numHashes; i++ { + copy(recentHashes[i][:], buf[offset:offset+32]) + offset += 32 + } + + return recentHeight, recentHashes, nil +} + +// PutStartBlock stores the provided start block stamp to the database. +func (mtx *managerTx) PutRecentBlocks(recentHeight int32, recentHashes []btcwire.ShaHash) error { + bucket := (*bolt.Tx)(mtx).Bucket(syncBucketName) + + // The serialized recent blocks format is: + // + // + // 4 bytes recent block height + 4 bytes number of hashes + raw hashes + // at 32 bytes each. + numHashes := uint32(len(recentHashes)) + buf := make([]byte, 8+(numHashes*32)) + binary.LittleEndian.PutUint32(buf[0:4], uint32(recentHeight)) + binary.LittleEndian.PutUint32(buf[4:8], numHashes) + offset := 8 + for i := uint32(0); i < numHashes; i++ { + copy(buf[offset:offset+32], recentHashes[i][:]) + offset += 32 + } + + err := bucket.Put(recentBlocksName, buf) + if err != nil { + str := "failed to store recent blocks" + return managerError(ErrDatabase, str, err) + } + return nil +} + +// managerDB provides transactional facilities to read and write the address +// manager data to a bolt database. +type managerDB struct { + db *bolt.DB + version uint32 + created time.Time +} + +// Close releases all database resources. All transactions must be closed +// before closing the database. +func (db *managerDB) Close() error { + if err := db.db.Close(); err != nil { + str := "failed to close database" + return managerError(ErrDatabase, str, err) + } + + return nil +} + +// View executes the passed function within the context of a managed read-only +// transaction. Any error that is returned from the passed function is returned +// from this function. +func (db *managerDB) View(fn func(tx *managerTx) error) error { + err := db.db.View(func(tx *bolt.Tx) error { + return fn((*managerTx)(tx)) + }) + if err != nil { + // Ensure the returned error is a ManagerError. + if _, ok := err.(ManagerError); !ok { + str := "failed during database read transaction" + return managerError(ErrDatabase, str, err) + } + return err + } + + return nil +} + +// Update executes the passed function within the context of a read-write +// managed transaction. The transaction is committed if no error is returned +// from the function. On the other hand, the entire transaction is rolled back +// if an error is returned. Any error that is returned from the passed function +// or returned from the commit is returned from this function. +func (db *managerDB) Update(fn func(tx *managerTx) error) error { + err := db.db.Update(func(tx *bolt.Tx) error { + return fn((*managerTx)(tx)) + }) + if err != nil { + // Ensure the returned error is a ManagerError. + if _, ok := err.(ManagerError); !ok { + str := "failed during database write transaction" + return managerError(ErrDatabase, str, err) + } + return err + } + + return nil +} + +// CopyDB copies the entire database to the provided new database path. A +// reader transaction is maintained during the copy so it is safe to continue +// using the database while a copy is in progress. +func (db *managerDB) CopyDB(newDbPath string) error { + err := db.db.View(func(tx *bolt.Tx) error { + if err := tx.CopyFile(newDbPath, 0600); err != nil { + str := "failed to copy database" + return managerError(ErrDatabase, str, err) + } + + return nil + }) + if err != nil { + // Ensure the returned error is a ManagerError. + if _, ok := err.(ManagerError); !ok { + str := "failed during database copy" + return managerError(ErrDatabase, str, err) + } + return err + } + + return nil +} + +// WriteTo writes the entire database to the provided writer. A reader +// transaction is maintained during the copy so it is safe to continue using the +// database while a copy is in progress. +func (db *managerDB) WriteTo(w io.Writer) error { + err := db.db.View(func(tx *bolt.Tx) error { + if err := tx.Copy(w); err != nil { + str := "failed to copy database" + return managerError(ErrDatabase, str, err) + } + + return nil + }) + if err != nil { + // Ensure the returned error is a ManagerError. + if _, ok := err.(ManagerError); !ok { + str := "failed during database copy" + return managerError(ErrDatabase, str, err) + } + return err + } + + return nil +} + +// openOrCreateDB opens the database at the provided path or creates and +// initializes it if it does not already exist. It also provides facilities to +// upgrade the database to newer versions. +func openOrCreateDB(dbPath string) (*managerDB, error) { + db, err := bolt.Open(dbPath, 0600, nil) + if err != nil { + str := "failed to open database" + return nil, managerError(ErrDatabase, str, err) + } + + // Initialize the buckets and main db fields as needed. + var version uint32 + var createDate uint64 + err = db.Update(func(tx *bolt.Tx) error { + mainBucket, err := tx.CreateBucketIfNotExists(mainBucketName) + if err != nil { + str := "failed to create main bucket" + return managerError(ErrDatabase, str, err) + } + + _, err = tx.CreateBucketIfNotExists(addrBucketName) + if err != nil { + str := "failed to create address bucket" + return managerError(ErrDatabase, str, err) + } + + _, err = tx.CreateBucketIfNotExists(acctBucketName) + if err != nil { + str := "failed to create account bucket" + return managerError(ErrDatabase, str, err) + } + + _, err = tx.CreateBucketIfNotExists(addrAcctIdxBucketName) + if err != nil { + str := "failed to create address index bucket" + return managerError(ErrDatabase, str, err) + } + + _, err = tx.CreateBucketIfNotExists(syncBucketName) + if err != nil { + str := "failed to create sync bucket" + return managerError(ErrDatabase, str, err) + } + + // Save the most recent database version if it isn't already + // there, otherwise keep track of it for potential upgrades. + verBytes := mainBucket.Get(dbVersionName) + if verBytes == nil { + version = LatestDbVersion + + var buf [4]byte + binary.LittleEndian.PutUint32(buf[:], version) + err := mainBucket.Put(dbVersionName, buf[:]) + if err != nil { + str := "failed to store latest database version" + return managerError(ErrDatabase, str, err) + } + } else { + version = binary.LittleEndian.Uint32(verBytes) + } + + createBytes := mainBucket.Get(dbCreateDateName) + if createBytes == nil { + createDate = uint64(time.Now().Unix()) + var buf [8]byte + binary.LittleEndian.PutUint64(buf[:], createDate) + err := mainBucket.Put(dbCreateDateName, buf[:]) + if err != nil { + str := "failed to store database creation time" + return managerError(ErrDatabase, str, err) + } + } else { + createDate = binary.LittleEndian.Uint64(createBytes) + } + + return nil + }) + if err != nil { + str := "failed to update database" + return nil, managerError(ErrDatabase, str, err) + } + + // Upgrade the database as needed. + if version < LatestDbVersion { + // No upgrades yet. + } + + return &managerDB{ + db: db, + version: version, + created: time.Unix(int64(createDate), 0), + }, nil +} diff --git a/waddrmgr/doc.go b/waddrmgr/doc.go new file mode 100644 index 0000000..7e56b9e --- /dev/null +++ b/waddrmgr/doc.go @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2014 Conformal Systems LLC + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* +Package waddrmgr provides a secure hierarchical deterministic wallet address +manager. + +Overview + +One of the fundamental jobs of a wallet is to manage addresses, private keys, +and script data associated with them. At a high level, this package provides +the facilities to perform this task with a focus on security and also allows +recovery through the use of hierarchical deterministic keys (BIP0032) generated +from a caller provided seed. The specific structure used is as described in +BIP0044. This setup means as long as the user writes the seed down (even better +is to use a mnemonic for the seed), all their addresses and private keys can be +regenerated from the seed. + +There are two master keys which are protected by two independent passphrases. +One is intended for public facing data, while the other is intended for private +data. The public password can be hardcoded for callers who don't want the +additional public data protection or the same password can be used if a single +password is desired. These choices provide a usability versus security +tradeoff. However, keep in mind that extended hd keys, as called out in BIP0032 +need to be handled more carefully than normal EC public keys because they can be +used to generate all future addresses. While this is part of what makes them +attractive, it also means an attacker getting access to your extended public key +for an account will allow them to know all addresses you will use and hence +reduces privacy. For this reason, it is highly recommended that you do not hard +code a password which allows any attacker who gets a copy of your address +manager database to access your effectively plain text extended public keys. + +Each master key in turn protects the three real encryption keys (called crypto +keys) for public, private, and script data. Some examples include payment +addresses, extended hd keys, and scripts associated with pay-to-script-hash +addresses. This scheme makes changing passphrases more efficient since only the +crypto keys need to be re-encrypted versus every single piece of information +(which is what is needed for *rekeying*). This results in a fully encrypted +database where access to it does not compromise address, key, or script privacy. +This differs from the handling by other wallets at the time of this writing in +that they divulge your addresses, and worse, some even expose the chain code +which can be used by the attacker to know all future addresses that will be +used. + +The address manager is also hardened against memory scrapers. This is +accomplished by typically having the address manager locked meaning no private +keys or scripts are in memory. Unlocking the address manager causes the crypto +private and script keys to be decrypted and loaded in memory which in turn are +used to decrypt private keys and scripts on demand. Relocking the address +manager actively zeros all private material from memory. In addition, temp +private key material used internally is zeroed as soon as it's used. + +Locking and Unlocking + +As previously mentioned, this package provide facilities for locking and +unlocking the address manager to protect access to private material and remove +it from memory when locked. The Lock, Unlock, and IsLocked functions are used +for this purpose. + +Creating a New Address Manager + +A new address manager is created via the Create function. This function accepts +the path to a database file to create, passphrases, network, and perhaps most +importantly, a cryptographically random seed which is used to generate the +master node of the hierarchical deterministic keychain which allows all +addresses and private keys to be recovered with only the seed. The GenerateSeed +function in the hdkeychain package can be used as a convenient way to create a +random seed for use with this function. The address manager is locked +immediately upon being created. + +Opening an Existing Address Manager + +An existing address manager is opened via the Open function. This function +accepts the path to the existing database file, the public passphrase, and +network. The address manager is opened locked as expected since the open +function does not take the private passphrase to unlock it. + +Closing the Address Manager + +The Close method should be called on the address manager when the caller is done +with it. While it is not required, it is recommended because it sanely shuts +down the database and ensures all private and public key material is purged from +memory. + +Managed Addresses + +Each address returned by the address manager satisifies the ManagedAddress +interface as well as either the ManagedPubKeyAddress or ManagedScriptAddress +interfaces. These interfaces provide the means to obtain relevant information +about the addresses such as their private keys and scripts. + +Chained Addresses + +Most callers will make use of the chained addresses for normal operations. +Internal addresses are intended for internal wallet uses such as change outputs, +while external addresses are intended for uses such payment addresses that are +shared. The NextInternalAddresses and NextExternalAddresses functions provide +the means to acquire one or more of the next addresses that have not already +been provided. In addition, the LastInternalAddress and LastExternalAddress +functions can be used to get the most recently provided internal and external +address, respectively. + +Requesting Existing Addresses + +In addition to generating new addresses, access to old addresses is often +required. Most notably, to sign transactions in order to redeem them. The +Address function provides this capability and returns a ManagedAddress + +Importing Addresses + +While the recommended approach is to use the chained addresses discussed above +because they can be deterministically regenerated to avoid losing funds as long +as the user has the master seed, there are many addresses that already exist, +and as a result, this package provides the ability to import existing private +keys in Wallet Import Format (WIF) and hence the associated public key and +address. + +Importing Scripts + +In order to support pay-to-script-hash transactions, the script must be securely +stored as it is needed to redeem the transaction. This can be useful for a +variety of scenarios, however the most common use is currently multi-signature +transactions. + +Syncing + +The address manager also supports storing and retrieving a block hash and height +which the manager is known to have all addresses synced through. The manager +itself does not have any notion of which addresses are synced or not. It only +provides the storage as a convenience for the caller. + +Network + +The address manager must be associated with a given network in order to provide +appropriate addresses and reject imported addresses and scripts which don't +apply to the associated network. + +Errors + +All errors returned from this package are of type waddrmgr.ManagerError. This +allows the caller to programmatically ascertain the specific reasons for failure +by examining the ErrorCode field of the type asserted ManagerError. For certain +error codes, as documented the specific error codes, the underlying error will +be contained in the Err field. + +Bitcoin Improvement Proposals + +This package includes concepts outlined by the following BIPs: + + BIP0032 (https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) + BIP0043 (https://github.com/bitcoin/bips/blob/master/bip-0043.mediawiki) + BIP0044 (https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki) +*/ +package waddrmgr diff --git a/waddrmgr/error.go b/waddrmgr/error.go new file mode 100644 index 0000000..1a7685e --- /dev/null +++ b/waddrmgr/error.go @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2014 Conformal Systems LLC + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package waddrmgr + +import ( + "fmt" + "strconv" + + "github.com/conformal/btcutil/hdkeychain" +) + +var ( + // errAlreadyExists is the common error description used for the + // ErrAlreadyExists error code. + errAlreadyExists = "the specified address manager already exists" + + // errCoinTypeTooHigh is the common error description used for the + // ErrCoinTypeTooHigh error code. + errCoinTypeTooHigh = "coin type may not exceed " + + strconv.FormatUint(hdkeychain.HardenedKeyStart-1, 10) + + // errAcctTooHigh is the common error description used for the + // ErrAccountNumTooHigh error code. + errAcctTooHigh = "account number may not exceed " + + strconv.FormatUint(hdkeychain.HardenedKeyStart-1, 10) + + // errLocked is the common error description used for the ErrLocked + // error code. + errLocked = "address manager is locked" + + // errWatchingOnly is the common error description used for the + // ErrWatchingOnly error code. + errWatchingOnly = "address manager is watching-only" +) + +// ErrorCode identifies a kind of error. +type ErrorCode int + +// These constants are used to identify a specific ManagerError. +const ( + // ErrDatabase indicates an error with the underlying database. When + // this error code is set, the Err field of the ManagerError will be + // set to the underlying error returned from the database. + ErrDatabase ErrorCode = iota + + // ErrKeyChain indicates an error with the key chain typically either + // due to the inability to create and extended key or deriving a child + // extended key. When this error code is set, the Err field of the + // ManagerError will be set to the underlying error. + ErrKeyChain + + // ErrCrypto indicates an error with the cryptography related operations + // such as decrypting or encrypting data, parsing an EC public key, + // or deriving a secret key from a password. When this error code is + // set, the Err field of the ManagerError will be set to the underlying + // error. + ErrCrypto + + // ErrNoExist indicates the specified database does not exist. + ErrNoExist + + // ErrAlreadyExists indicates the specified database already exists. + ErrAlreadyExists + + // ErrCoinTypeTooHigh indicates the coin type specified in the provided + // network parameters is higher than the max allowed value as defined + // by the maxCoinType constant. + ErrCoinTypeTooHigh + + // ErrAccountNumTooHigh indicates the specified account number is higher + // than the max allowed value as defined by the MaxAccountNum constant. + ErrAccountNumTooHigh + + // ErrLocked indicates the an operation which requires the address + // manager to be unlocked was requested on a locked address manager. + ErrLocked + + // ErrWatchingOnly indicates the an operation which requires the address + // manager to have access to private data was requested on a + // watching-only address manager. + ErrWatchingOnly + + // ErrInvalidAccount indicates the requested account is not valid. + ErrInvalidAccount + + // ErrAddressNotFound indicates the requested address is not known to + // the address manager. + ErrAddressNotFound + + // ErrAccountNotFound indicates the requested account is not known to + // the address manager. + ErrAccountNotFound + + // ErrDuplicate indicates an address already exists. + ErrDuplicate + + // ErrTooManyAddresses indicates more than the maximum allowed number of + // addresses per account have been requested. + ErrTooManyAddresses + + // ErrWrongPassphrase inidicates the specified password is incorrect. + // This could be for either the public and private master keys. + ErrWrongPassphrase + + // ErrWrongNet indicates the private key to be imported is not for the + // the same network the account mangaer is configured for. + ErrWrongNet +) + +// Map of ErrorCode values back to their constant names for pretty printing. +var errorCodeStrings = map[ErrorCode]string{ + ErrDatabase: "ErrDatabase", + ErrKeyChain: "ErrKeyChain", + ErrCrypto: "ErrCrypto", + ErrNoExist: "ErrNoExist", + ErrAlreadyExists: "ErrAlreadyExists", + ErrCoinTypeTooHigh: "ErrCoinTypeTooHigh", + ErrAccountNumTooHigh: "ErrAccountNumTooHigh", + ErrLocked: "ErrLocked", + ErrWatchingOnly: "ErrWatchingOnly", + ErrInvalidAccount: "ErrInvalidAccount", + ErrAddressNotFound: "ErrAddressNotFound", + ErrAccountNotFound: "ErrAccountNotFound", + ErrDuplicate: "ErrDuplicate", + ErrTooManyAddresses: "ErrTooManyAddresses", + ErrWrongPassphrase: "ErrWrongPassphrase", + ErrWrongNet: "ErrWrongNet", +} + +// String returns the ErrorCode as a human-readable name. +func (e ErrorCode) String() string { + if s := errorCodeStrings[e]; s != "" { + return s + } + return fmt.Sprintf("Unknown ErrorCode (%d)", int(e)) +} + +// ManagerError provides a single type for errors that can happen during address +// manager operation. It is used to indicate several types of failures +// including errors with caller requests such as invalid accounts or requesting +// private keys against a locked address manager, errors with the database +// (ErrDatabase), errors with key chain derivation (ErrKeyChain), and errors +// related to crypto (ErrCrypto). +// +// The caller can use type assertions to determine if an error is a ManagerError +// and access the ErrorCode field to ascertain the specific reason for the +// failure. +// +// The ErrDatabase, ErrKeyChain, and ErrCrypto error codes will also have the +// Err field set with the underlying error. +type ManagerError struct { + ErrorCode ErrorCode // Describes the kind of error + Description string // Human readable description of the issue + Err error // Underlying error +} + +// Error satisfies the error interface and prints human-readable errors. +func (e ManagerError) Error() string { + if e.Err != nil { + return e.Description + ": " + e.Err.Error() + } + return e.Description +} + +// managerError creates a ManagerError given a set of arguments. +func managerError(c ErrorCode, desc string, err error) ManagerError { + return ManagerError{ErrorCode: c, Description: desc, Err: err} +} diff --git a/waddrmgr/error_test.go b/waddrmgr/error_test.go new file mode 100644 index 0000000..e6f4a67 --- /dev/null +++ b/waddrmgr/error_test.go @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2014 Conformal Systems LLC + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package waddrmgr_test + +import ( + "fmt" + "testing" + + "github.com/conformal/btcwallet/waddrmgr" +) + +// TestErrorCodeStringer tests the stringized output for the ErrorCode type. +func TestErrorCodeStringer(t *testing.T) { + tests := []struct { + in waddrmgr.ErrorCode + want string + }{ + {waddrmgr.ErrDatabase, "ErrDatabase"}, + {waddrmgr.ErrKeyChain, "ErrKeyChain"}, + {waddrmgr.ErrCrypto, "ErrCrypto"}, + {waddrmgr.ErrNoExist, "ErrNoExist"}, + {waddrmgr.ErrAlreadyExists, "ErrAlreadyExists"}, + {waddrmgr.ErrCoinTypeTooHigh, "ErrCoinTypeTooHigh"}, + {waddrmgr.ErrAccountNumTooHigh, "ErrAccountNumTooHigh"}, + {waddrmgr.ErrLocked, "ErrLocked"}, + {waddrmgr.ErrWatchingOnly, "ErrWatchingOnly"}, + {waddrmgr.ErrInvalidAccount, "ErrInvalidAccount"}, + {waddrmgr.ErrAddressNotFound, "ErrAddressNotFound"}, + {waddrmgr.ErrAccountNotFound, "ErrAccountNotFound"}, + {waddrmgr.ErrDuplicate, "ErrDuplicate"}, + {waddrmgr.ErrTooManyAddresses, "ErrTooManyAddresses"}, + {waddrmgr.ErrWrongPassphrase, "ErrWrongPassphrase"}, + {waddrmgr.ErrWrongNet, "ErrWrongNet"}, + {0xffff, "Unknown ErrorCode (65535)"}, + } + t.Logf("Running %d tests", len(tests)) + for i, test := range tests { + result := test.in.String() + if result != test.want { + t.Errorf("String #%d\ngot: %s\nwant: %s", i, result, + test.want) + continue + } + } +} + +// TestManagerError tests the error output for the ManagerError type. +func TestManagerError(t *testing.T) { + tests := []struct { + in waddrmgr.ManagerError + want string + }{ + // Manager level error. + { + waddrmgr.ManagerError{Description: "human-readable error"}, + "human-readable error", + }, + + // Encapsulated database error. + { + waddrmgr.ManagerError{ + Description: "failed to store master private " + + "key parameters", + ErrorCode: waddrmgr.ErrDatabase, + Err: fmt.Errorf("underlying db error"), + }, + "failed to store master private key parameters: " + + "underlying db error", + }, + + // Encapsulated key chain error. + { + waddrmgr.ManagerError{ + Description: "failed to derive extended key " + + "branch 0", + ErrorCode: waddrmgr.ErrKeyChain, + Err: fmt.Errorf("underlying error"), + }, + "failed to derive extended key branch 0: underlying " + + "error", + }, + + // Encapsulated crypto error. + { + waddrmgr.ManagerError{ + Description: "failed to decrypt account 0 " + + "private key", + ErrorCode: waddrmgr.ErrCrypto, + Err: fmt.Errorf("underlying error"), + }, + "failed to decrypt account 0 private key: underlying " + + "error", + }, + } + + t.Logf("Running %d tests", len(tests)) + for i, test := range tests { + result := test.in.Error() + if result != test.want { + t.Errorf("Error #%d\ngot: %s\nwant: %s", i, result, + test.want) + continue + } + } +} diff --git a/waddrmgr/internal_test.go b/waddrmgr/internal_test.go new file mode 100644 index 0000000..3485d82 --- /dev/null +++ b/waddrmgr/internal_test.go @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2014 Conformal Systems LLC + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* +This test file is part of the waddrmgr package rather than than the +waddrmgr_test package so it can bridge access to the internals to properly test +cases which are either not possible or can't reliably be tested via the public +interface. The functions are only exported while the tests are being run. +*/ + +package waddrmgr + +import ( + "github.com/conformal/btcwallet/snacl" +) + +// TstMaxRecentHashes makes the unexported maxRecentHashes constant available +// when tests are run. +var TstMaxRecentHashes = maxRecentHashes + +// TstSetScryptParams allows the scrypt parameters to be set to much lower +// values while the tests are running so they are faster. +func TstSetScryptParams(n, r, p int) { + scryptN = n + scryptR = r + scryptP = p +} + +// TstReplaceNewSecretKeyFunc replaces the new secret key generation function +// with a version that intentionally fails. +func TstReplaceNewSecretKeyFunc() { + newSecretKey = func(passphrase *[]byte) (*snacl.SecretKey, error) { + return nil, snacl.ErrDecryptFailed + } +} + +// TstResetNewSecretKeyFunc resets the new secret key generation function to the +// original version. +func TstResetNewSecretKeyFunc() { + newSecretKey = defaultNewSecretKey +} + +// TstCheckPublicPassphrase return true if the provided public passphrase is +// correct for the manager. +func (m *Manager) TstCheckPublicPassphrase(pubPassphrase []byte) bool { + secretKey := snacl.SecretKey{Key: &snacl.CryptoKey{}} + secretKey.Parameters = m.masterKeyPub.Parameters + err := secretKey.DeriveKey(&pubPassphrase) + return err == nil +} diff --git a/waddrmgr/manager.go b/waddrmgr/manager.go new file mode 100644 index 0000000..9412ca7 --- /dev/null +++ b/waddrmgr/manager.go @@ -0,0 +1,1821 @@ +/* + * Copyright (c) 2014 Conformal Systems LLC + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package waddrmgr + +import ( + "fmt" + "io" + "os" + "sync" + + "github.com/conformal/btcec" + "github.com/conformal/btcnet" + "github.com/conformal/btcutil" + "github.com/conformal/btcutil/hdkeychain" + "github.com/conformal/btcwallet/snacl" + "github.com/conformal/btcwire" +) + +const ( + // MaxAccountNum is the maximum allowed account number. This value was + // chosen because accounts are hardened children and therefore must + // not exceed the hardened child range of extended keys and it provides + // a reserved account at the top of the range for supporting imported + // addresses. + MaxAccountNum = hdkeychain.HardenedKeyStart - 2 // 2^31 - 2 + + // MaxAddressesPerAccount is the maximum allowed number of addresses + // per account number. This value is based on the limitation of + // the underlying hierarchical deterministic key derivation. + MaxAddressesPerAccount = hdkeychain.HardenedKeyStart - 1 + + // importedAddrAccount is the account number to use for all imported + // addresses. This is useful since normal accounts are derived from the + // root hierarchical deterministic key and imported addresses do not + // fit into that model. + ImportedAddrAccount = MaxAccountNum + 1 // 2^31 - 1 + + // defaultAccountNum is the number of the default account. + defaultAccountNum = 0 + + // The hierarchy described by BIP0043 is: + // m/'/* + // This is further extended by BIP0044 to: + // m/44'/'/'//

+ // + // The branch is 0 for external addresses and 1 for internal addresses. + + // maxCoinType is the maximum allowed coin type used when structuring + // the BIP0044 multi-account hierarchy. This value is based on the + // limitation of the underlying hierarchical deterministic key + // derivation. + maxCoinType = hdkeychain.HardenedKeyStart - 1 + + // externalBranch is the child number to use when performing BIP0044 + // style hierarchical deterministic key derivation for the external + // branch. + externalBranch uint32 = 0 + + // internalBranch is the child number to use when performing BIP0044 + // style hierarchical deterministic key derivation for the internal + // branch. + internalBranch uint32 = 1 +) + +var ( + // scryptN, scryptR, and scryptP are the parameters used for scrypt + // password-based key derivation. + scryptN = 262144 // 2^18 + scryptR = 8 + scryptP = 1 +) + +// addrKey is used to uniquely identify an address even when those addresses +// would end up being the same bitcoin address (as is the case for pay-to-pubkey +// and pay-to-pubkey-hash style of addresses). +type addrKey string + +// accountInfo houses the current state of the internal and external branches +// of an account along with the extended keys needed to derive new keys. It +// also handles locking by keeping an encrypted version of the serialized +// private extended key so the unencrypted versions can be cleared from memory +// when the address manager is locked. +type accountInfo struct { + // The account key is used to derive the branches which in turn derive + // the internal and external addresses. + // The accountKeyPriv will be nil when the address manager is locked. + acctKeyEncrypted []byte + acctKeyPriv *hdkeychain.ExtendedKey + acctKeyPub *hdkeychain.ExtendedKey + + // The external branch is used for all addresses which are intended + // for external use. + nextExternalIndex uint32 + lastExternalAddr ManagedAddress + + // The internal branch is used for all adddresses which are only + // intended for internal wallet use such as change addresses. + nextInternalIndex uint32 + lastInternalAddr ManagedAddress +} + +// unlockDeriveInfo houses the information needed to derive a private key for a +// managed address when the address manager is unlocked. See the deriveOnUnlock +// field in the Manager struct for more details on how this is used. +type unlockDeriveInfo struct { + managedAddr *managedAddress + branch uint32 + index uint32 +} + +// defaultNewSecretKey returns a new secret key. See newSecretKey. +func defaultNewSecretKey(passphrase *[]byte) (*snacl.SecretKey, error) { + return snacl.NewSecretKey(passphrase, scryptN, scryptR, scryptP) +} + +// newSecretKey is used as a way to replace the new secret key generation +// function used so tests can provide a version that fails for testing error +// paths. +var newSecretKey = defaultNewSecretKey + +// Manager represents a concurrency safe crypto currency address manager and +// key store. +type Manager struct { + mtx sync.RWMutex + + db *managerDB + net *btcnet.Params + addrs map[addrKey]ManagedAddress + syncState syncState + watchingOnly bool + locked bool + closed bool + + // acctInfo houses information about accounts including what is needed + // to generate deterministic chained keys for each created account. + acctInfo map[uint32]*accountInfo + + // masterKeyPub is the secret key used to secure the cryptoKeyPub key + // and masterKeyPriv is the secret key used to secure the cryptoKeyPriv + // key. This approach is used because it makes changing the passwords + // much simpler as it then becomes just changing these keys. It also + // provides future flexibility. + // + // NOTE: This is not the same thing as BIP0032 master node extended + // key. + // + // The underlying master private key will be zeroed when the address + // manager is locked. + masterKeyPub *snacl.SecretKey + masterKeyPriv *snacl.SecretKey + + // cryptoKeyPub is the key used to encrypt public extended keys and + // addresses. + cryptoKeyPub *snacl.CryptoKey + + // cryptoKeyPriv is the key used to encrypt private data such as the + // master hierarchical deterministic extended key. + // + // This key will be zeroed when the address manager is locked. + cryptoKeyPrivEncrypted []byte + cryptoKeyPriv *snacl.CryptoKey + + // cryptoKeyScript is the key used to encrypt script data. + // + // This key will be zeroed when the address manager is locked. + cryptoKeyScriptEncrypted []byte + cryptoKeyScript *snacl.CryptoKey + + // deriveOnUnlock is a list of private keys which needs to be derived + // on the next unlock. This occurs when a public address is derived + // while the address manager is locked since it does not have access to + // the private extended key (hence nor the underlying private key) in + // order to encrypt it. + deriveOnUnlock []*unlockDeriveInfo +} + +// lock performs a best try effort to remove and zero all secret keys associated +// with the address manager. +// +// This function MUST be called with the manager lock held for writes. +func (m *Manager) lock() { + // Clear all of the account private keys. + for _, acctInfo := range m.acctInfo { + if acctInfo.acctKeyPriv != nil { + acctInfo.acctKeyPriv.Zero() + } + acctInfo.acctKeyPriv = nil + } + + // Remove clear text private keys and scripts from all address entries. + for _, ma := range m.addrs { + switch addr := ma.(type) { + case *managedAddress: + addr.lock() + case *scriptAddress: + addr.lock() + } + } + + // Remove clear text private master and crypto keys from memory. + m.cryptoKeyScript.Zero() + m.cryptoKeyPriv.Zero() + m.masterKeyPriv.Zero() + + // NOTE: m.cryptoKeyPub is intentionally not cleared here as the address + // manager needs to be able to continue to read and decrypt public data + // which uses a separate derived key from the database even when it is + // locked. + + m.locked = true +} + +// zeroSensitivePublicData performs a best try effort to remove and zero all +// sensitive public data associated with the address manager such as +// hierarchical deterministic extended public keys and the crypto public keys. +func (m *Manager) zeroSensitivePublicData() { + // Clear all of the account private keys. + for _, acctInfo := range m.acctInfo { + acctInfo.acctKeyPub.Zero() + acctInfo.acctKeyPub = nil + } + + // Remove clear text public master and crypto keys from memory. + m.cryptoKeyPub.Zero() + m.masterKeyPub.Zero() +} + +// Close cleanly shuts down the underlying database and syncs all data. It also +// makes a best try effort to remove and zero all private key and sensitive +// public key material associated with the address manager. +func (m *Manager) Close() error { + m.mtx.Lock() + defer m.mtx.Unlock() + + // Attempt to clear private key material from memory. + if !m.watchingOnly && !m.locked { + m.lock() + } + + // Attempt to clear sensitive public key material from memory too. + m.zeroSensitivePublicData() + + if err := m.db.Close(); err != nil { + return err + } + + m.closed = true + return nil +} + +// keyToManaged returns a new managed address for the provided derived key and +// its derivation path which consists of the account, branch, and index. +// +// The passed derivedKey is zeroed after the new address is created. +// +// This function MUST be called with the manager lock held for writes. +func (m *Manager) keyToManaged(derivedKey *hdkeychain.ExtendedKey, account, branch, index uint32) (ManagedAddress, error) { + // Create a new managed address based on the public or private key + // depending on whether the passed key is private. Also, zero the + // key after creating the managed address from it. + ma, err := newManagedAddressFromExtKey(m, account, derivedKey) + defer derivedKey.Zero() + if err != nil { + return nil, err + } + if !derivedKey.IsPrivate() { + // Add the managed address to the list of addresses that need + // their private keys derived when the address manager is next + // unlocked. + info := unlockDeriveInfo{ + managedAddr: ma, + branch: branch, + index: index, + } + m.deriveOnUnlock = append(m.deriveOnUnlock, &info) + } + if branch == internalBranch { + ma.internal = true + } + + return ma, nil +} + +// deriveKey returns either a public or private derived extended key based on +// the private flag for the given an account info, branch, and index. +func (m *Manager) deriveKey(acctInfo *accountInfo, branch, index uint32, private bool) (*hdkeychain.ExtendedKey, error) { + // Choose the public or private extended key based on whether or not + // the private flag was specified. This, in turn, allows for public or + // private child derivation. + acctKey := acctInfo.acctKeyPub + if private { + acctKey = acctInfo.acctKeyPriv + } + + // Derive and return the key. + branchKey, err := acctKey.Child(branch) + if err != nil { + str := fmt.Sprintf("failed to derive extended key branch %d", + branch) + return nil, managerError(ErrKeyChain, str, err) + } + addressKey, err := branchKey.Child(index) + branchKey.Zero() // Zero branch key after it's used. + if err != nil { + str := fmt.Sprintf("failed to derive child extended key -- "+ + "branch %d, child %d", + branch, index) + return nil, managerError(ErrKeyChain, str, err) + } + return addressKey, nil +} + +// loadAccountInfo attempts to load and cache information about the given +// account from the database. This includes what is necessary to derive new +// keys for it and track the state of the internal and external branches. +// +// This function MUST be called with the manager lock held for writes. +func (m *Manager) loadAccountInfo(account uint32) (*accountInfo, error) { + // Return the account info from cache if it's available. + if acctInfo, ok := m.acctInfo[account]; ok { + return acctInfo, nil + } + + // The account is either invalid or just wasn't cached, so attempt to + // load the information from the database. + var rowInterface interface{} + err := m.db.View(func(tx *managerTx) error { + var err error + rowInterface, err = tx.FetchAccountInfo(account) + return err + }) + if err != nil { + return nil, err + } + + // Ensure the account type is a BIP0044 account. + row, ok := rowInterface.(*dbBIP0044AccountRow) + if !ok { + str := fmt.Sprintf("unsupported account type %T", row) + err = managerError(ErrDatabase, str, nil) + } + + // Use the crypto public key to decrypt the account public extended key. + serializedKeyPub, err := m.cryptoKeyPub.Decrypt(row.pubKeyEncrypted) + if err != nil { + str := fmt.Sprintf("failed to decrypt public key for account %d", + account) + return nil, managerError(ErrCrypto, str, err) + } + acctKeyPub, err := hdkeychain.NewKeyFromString(string(serializedKeyPub)) + if err != nil { + str := fmt.Sprintf("failed to create extended public key for "+ + "account %d", account) + return nil, managerError(ErrKeyChain, str, err) + } + + // Create the new account info with the known information. The rest + // of the fields are filled out below. + acctInfo := &accountInfo{ + acctKeyEncrypted: row.privKeyEncrypted, + acctKeyPub: acctKeyPub, + nextExternalIndex: row.nextExternalIndex, + nextInternalIndex: row.nextInternalIndex, + } + + if !m.locked { + // Use the crypto private key to decrypt the account private + // extended keys. + decrypted, err := m.cryptoKeyPriv.Decrypt(acctInfo.acctKeyEncrypted) + if err != nil { + str := fmt.Sprintf("failed to decrypt private key for "+ + "account %d", account) + return nil, managerError(ErrCrypto, str, err) + } + + acctKeyPriv, err := hdkeychain.NewKeyFromString(string(decrypted)) + if err != nil { + str := fmt.Sprintf("failed to create extended private "+ + "key for account %d", account) + return nil, managerError(ErrKeyChain, str, err) + } + acctInfo.acctKeyPriv = acctKeyPriv + } + + // Derive and cache the managed address for the last external address. + branch, index := externalBranch, row.nextExternalIndex + if index > 0 { + index-- + } + lastExtKey, err := m.deriveKey(acctInfo, branch, index, !m.locked) + if err != nil { + return nil, err + } + lastExtAddr, err := m.keyToManaged(lastExtKey, account, branch, index) + if err != nil { + return nil, err + } + acctInfo.lastExternalAddr = lastExtAddr + + // Derive and cache the managed address for the last internal address. + branch, index = internalBranch, row.nextInternalIndex + if index > 0 { + index-- + } + lastIntKey, err := m.deriveKey(acctInfo, branch, index, !m.locked) + if err != nil { + return nil, err + } + lastIntAddr, err := m.keyToManaged(lastIntKey, account, branch, index) + if err != nil { + return nil, err + } + acctInfo.lastInternalAddr = lastIntAddr + + // Add it to the cache and return it when everything is successful. + m.acctInfo[account] = acctInfo + return acctInfo, nil +} + +// deriveKeyFromPath returns either a public or private derived extended key +// based on the private flag for the given an account, branch, and index. +// +// This function MUST be called with the manager lock held for writes. +func (m *Manager) deriveKeyFromPath(account, branch, index uint32, private bool) (*hdkeychain.ExtendedKey, error) { + // Look up the account key information. + acctInfo, err := m.loadAccountInfo(account) + if err != nil { + return nil, err + } + + return m.deriveKey(acctInfo, branch, index, private) +} + +// chainAddressRowToManaged returns a new managed address based on chained +// address data loaded from the database. +// +// This function MUST be called with the manager lock held for writes. +func (m *Manager) chainAddressRowToManaged(row *dbChainAddressRow) (ManagedAddress, error) { + addressKey, err := m.deriveKeyFromPath(row.account, row.branch, + row.index, !m.locked) + if err != nil { + return nil, err + } + + return m.keyToManaged(addressKey, row.account, row.branch, row.index) +} + +// importedAddressRowToManaged returns a new managed address based on imported +// address data loaded from the database. +func (m *Manager) importedAddressRowToManaged(row *dbImportedAddressRow) (ManagedAddress, error) { + // Use the crypto public key to decrypt the imported public key. + pubBytes, err := m.cryptoKeyPub.Decrypt(row.encryptedPubKey) + if err != nil { + str := "failed to decrypt public key for imported address" + return nil, managerError(ErrCrypto, str, err) + } + + pubKey, err := btcec.ParsePubKey(pubBytes, btcec.S256()) + if err != nil { + str := "invalid public key for imported address" + return nil, managerError(ErrCrypto, str, err) + } + + compressed := len(pubBytes) == btcec.PubKeyBytesLenCompressed + ma, err := newManagedAddressWithoutPrivKey(m, row.account, pubKey, + compressed) + if err != nil { + return nil, err + } + ma.privKeyEncrypted = row.encryptedPrivKey + ma.imported = true + + return ma, nil +} + +// scriptAddressRowToManaged returns a new managed address based on script +// address data loaded from the database. +func (m *Manager) scriptAddressRowToManaged(row *dbScriptAddressRow) (ManagedAddress, error) { + // Use the crypto public key to decrypt the imported script hash. + scriptHash, err := m.cryptoKeyPub.Decrypt(row.encryptedHash) + if err != nil { + str := "failed to decrypt imported script hash" + return nil, managerError(ErrCrypto, str, err) + } + + return newScriptAddress(m, row.account, scriptHash, row.encryptedScript) +} + +// rowInterfaceToManaged returns a new managed address based on the given +// address data loaded from the database. It will automatically select the +// appropriate type. +func (m *Manager) rowInterfaceToManaged(rowInterface interface{}) (ManagedAddress, error) { + switch row := rowInterface.(type) { + case *dbChainAddressRow: + return m.chainAddressRowToManaged(row) + + case *dbImportedAddressRow: + return m.importedAddressRowToManaged(row) + + case *dbScriptAddressRow: + return m.scriptAddressRowToManaged(row) + } + + str := fmt.Sprintf("unsupported address type %T", rowInterface) + return nil, managerError(ErrDatabase, str, nil) +} + +// loadAndCacheAddress attempts to load the passed address from the database and +// caches the associated managed address. +// +// This function MUST be called with the manager lock held for writes. +func (m *Manager) loadAndCacheAddress(address btcutil.Address) (ManagedAddress, error) { + // Attempt to load the raw address information from the database. + var rowInterface interface{} + err := m.db.View(func(tx *managerTx) error { + var err error + rowInterface, err = tx.FetchAddress(address.ScriptAddress()) + return err + }) + if err != nil { + return nil, err + } + + // Create a new managed address for the specific type of address based + // on type. + managedAddr, err := m.rowInterfaceToManaged(rowInterface) + if err != nil { + return nil, err + } + + // Cache and return the new managed address. + m.addrs[addrKey(managedAddr.Address().ScriptAddress())] = managedAddr + return managedAddr, nil +} + +// Address returns a managed address given the passed address if it is known +// to the address manager. A managed address differs from the passed address +// in that it also potentially contains extra information needed to sign +// transactions such as the associated private key for pay-to-pubkey and +// pay-to-pubkey-hash addresses and the script associated with +// pay-to-script-hash addresses. +func (m *Manager) Address(address btcutil.Address) (ManagedAddress, error) { + // Return the address from cache if it's available. + // + // NOTE: Not using a defer on the lock here since a write lock is + // needed if the lookup fails. + m.mtx.RLock() + if ma, ok := m.addrs[addrKey(address.ScriptAddress())]; ok { + m.mtx.RUnlock() + return ma, nil + } + m.mtx.RUnlock() + + m.mtx.Lock() + defer m.mtx.Unlock() + + // Attempt to load the address from the database. + return m.loadAndCacheAddress(address) +} + +// ChangePassphrase changes either the public or private passphrase to the +// provided value depending on the private flag. In order to change the private +// password, the address manager must not be watching-only. +func (m *Manager) ChangePassphrase(oldPassphrase, newPassphrase []byte, private bool) error { + // No private passphrase to change for a watching-only address manager. + if private && m.watchingOnly { + return managerError(ErrWatchingOnly, errWatchingOnly, nil) + } + + m.mtx.Lock() + defer m.mtx.Unlock() + + // Ensure the provided old passphrase is correct. This check is done + // using a copy of the appropriate master key depending on the private + // flag to ensure the current state is not altered. The temp key is + // cleared when done to avoid leaving a copy in memory. + var keyName string + secretKey := snacl.SecretKey{Key: &snacl.CryptoKey{}} + if private { + keyName = "private" + secretKey.Parameters = m.masterKeyPriv.Parameters + } else { + keyName = "public" + secretKey.Parameters = m.masterKeyPub.Parameters + } + if err := secretKey.DeriveKey(&oldPassphrase); err != nil { + if err == snacl.ErrInvalidPassword { + str := fmt.Sprintf("invalid passphrase for %s master "+ + "key", keyName) + return managerError(ErrWrongPassphrase, str, nil) + } + + str := fmt.Sprintf("failed to derive %s master key", keyName) + return managerError(ErrCrypto, str, err) + } + defer secretKey.Zero() + + // Generate a new master key from the passphrase which is used to secure + // the actual secret keys. + newMasterKey, err := newSecretKey(&newPassphrase) + if err != nil { + str := "failed to create new master private key" + return managerError(ErrCrypto, str, err) + } + newKeyParams := newMasterKey.Marshal() + + if private { + // Technically, the locked state could be checked here to only + // do the decrypts when the address manager is locked as the + // clear text keys are already available in memory when it is + // unlocked, but this is not a hot path, decryption is quite + // fast, and it's less cyclomatic complexity to simply decrypt + // in either case. + + // Re-encrypt the crypto private key using the new master + // private key. + decPriv, err := secretKey.Decrypt(m.cryptoKeyPrivEncrypted) + if err != nil { + str := "failed to decrypt crypto private key" + return managerError(ErrCrypto, str, err) + } + encPriv, err := newMasterKey.Encrypt(decPriv) + zero(decPriv) + if err != nil { + str := "failed to encrypt crypto private key" + return managerError(ErrCrypto, str, err) + } + + // Re-encrypt the crypto script key using the new master private + // key. + decScript, err := secretKey.Decrypt(m.cryptoKeyScriptEncrypted) + if err != nil { + str := "failed to decrypt crypto script key" + return managerError(ErrCrypto, str, err) + } + encScript, err := newMasterKey.Encrypt(decScript) + zero(decScript) + if err != nil { + str := "failed to encrypt crypto script key" + return managerError(ErrCrypto, str, err) + } + + // When the manager is locked, ensure the new clear text master + // key is cleared from memory now that it is no longer needed. + if m.locked { + newMasterKey.Zero() + } + + // Save the new keys and params to the the db in a single + // transaction. + err = m.db.Update(func(tx *managerTx) error { + err := tx.PutCryptoKeys(nil, encPriv, encScript) + if err != nil { + return err + } + + return tx.PutMasterKeyParams(nil, newKeyParams) + }) + if err != nil { + return err + } + + // Now that the db has been successfully updated, clear the old + // key and set the new one. + copy(m.cryptoKeyPrivEncrypted[:], encPriv) + copy(m.cryptoKeyScriptEncrypted[:], encScript) + m.masterKeyPriv.Zero() // Clear the old key. + m.masterKeyPriv = newMasterKey + } else { + // Re-encrypt the crypto public key using the new master public + // key. + encryptedPub, err := newMasterKey.Encrypt(m.cryptoKeyPub[:]) + if err != nil { + str := "failed to encrypt crypto public key" + return managerError(ErrCrypto, str, err) + } + + // Save the new keys and params to the the db in a single + // transaction. + err = m.db.Update(func(tx *managerTx) error { + err := tx.PutCryptoKeys(encryptedPub, nil, nil) + if err != nil { + return err + } + + return tx.PutMasterKeyParams(newKeyParams, nil) + }) + if err != nil { + return err + } + + // Now that the db has been successfully updated, clear the old + // key and set the new one. + m.masterKeyPub.Zero() + m.masterKeyPub = newMasterKey + } + + return nil +} + +// ExportWatchingOnly creates a new watching-only address manager backed by a +// database at the provided path. A watching-only address manager has all +// private keys removed which means it is not possible to create transactions +// which spend funds. +func (m *Manager) ExportWatchingOnly(newDbPath string, pubPassphrase []byte) (*Manager, error) { + m.mtx.RLock() + defer m.mtx.RUnlock() + + // Return an error if the specified database already exists. + if fileExists(newDbPath) { + return nil, managerError(ErrAlreadyExists, errAlreadyExists, nil) + } + + // Copy the existing manager database to the provided path. + if err := m.db.CopyDB(newDbPath); err != nil { + return nil, err + } + + // Open the copied database. + watchingDb, err := openOrCreateDB(newDbPath) + if err != nil { + return nil, err + } + + // Remove all private key material and mark the new database as watching + // only. + err = watchingDb.Update(func(tx *managerTx) error { + if err := tx.DeletePrivateKeys(); err != nil { + return err + } + + return tx.PutWatchingOnly(true) + }) + if err != nil { + return nil, err + } + + return loadManager(watchingDb, pubPassphrase, m.net) +} + +// Export writes the manager database to the provided writer. +func (m *Manager) Export(w io.Writer) error { + m.mtx.RLock() + defer m.mtx.RUnlock() + + // Copy the existing manager database to the provided path. + return m.db.WriteTo(w) +} + +// existsAddress returns whether or not the passed address is known to the +// address manager. +// +// This function MUST be called with the manager lock held for reads. +func (m *Manager) existsAddress(addressID []byte) (bool, error) { + // Check the in-memory map first since it's faster than a db access. + if _, ok := m.addrs[addrKey(addressID)]; ok { + return true, nil + } + + // Check the database if not already found above. + var exists bool + err := m.db.View(func(tx *managerTx) error { + exists = tx.ExistsAddress(addressID) + return nil + }) + if err != nil { + return false, err + } + + return exists, nil +} + +// ImportPrivateKey imports a WIF private key into the address manager. The +// imported address is created using either a compressed or uncompressed +// serialized public key, depending on the CompressPubKey bool of the WIF. +// +// All imported addresses will be part of the account defined by the +// ImportedAddrAccount constant. +// +// NOTE: When the address manager is watching-only, the private key itself will +// not be stored or available since it is private data. Instead, only the +// public key will be stored. This means it is paramount the private key is +// kept elsewhere as the watching-only address manager will NOT ever have access +// to it. +// +// This function will return an error if the address manager is locked and not +// watching-only, or not for the same network as the key trying to be imported. +// It will also return an error if the address already exists. Any other errors +// returned are generally unexpected. +func (m *Manager) ImportPrivateKey(wif *btcutil.WIF, bs *BlockStamp) (ManagedPubKeyAddress, error) { + // Ensure the address is intended for network the address manager is + // associated with. + if !wif.IsForNet(m.net) { + str := fmt.Sprintf("private key is not for the same network the "+ + "address manager is configured for (%s)", m.net.Name) + return nil, managerError(ErrWrongNet, str, nil) + } + + m.mtx.Lock() + defer m.mtx.Unlock() + + // The manager must be unlocked to encrypt the imported private key. + if m.locked && !m.watchingOnly { + return nil, managerError(ErrLocked, errLocked, nil) + } + + // Prevent duplicates. + serializedPubKey := wif.SerializePubKey() + pubKeyHash := btcutil.Hash160(serializedPubKey) + alreadyExists, err := m.existsAddress(pubKeyHash) + if err != nil { + return nil, err + } + if alreadyExists { + str := fmt.Sprintf("address for public key %x already exists", + serializedPubKey) + return nil, managerError(ErrDuplicate, str, nil) + } + + // Encrypt public key. + encryptedPubKey, err := m.cryptoKeyPub.Encrypt(serializedPubKey) + if err != nil { + str := fmt.Sprintf("failed to encrypt public key for %x", + serializedPubKey) + return nil, managerError(ErrCrypto, str, err) + } + + // Encrypt the private key when not a watching-only address manager. + var encryptedPrivKey []byte + if !m.watchingOnly { + privKeyBytes := wif.PrivKey.Serialize() + encryptedPrivKey, err = m.cryptoKeyPriv.Encrypt(privKeyBytes) + zero(privKeyBytes) + if err != nil { + str := fmt.Sprintf("failed to encrypt private key for %x", + serializedPubKey) + return nil, managerError(ErrCrypto, str, err) + } + } + + // The start block needs to be updated when the newly imported address + // is before the current one. + updateStartBlock := bs.Height < m.syncState.startBlock.Height + + // Save the new imported address to the db and update start block (if + // needed) in a single transaction. + err = m.db.Update(func(tx *managerTx) error { + err := tx.PutImportedAddress(pubKeyHash, ImportedAddrAccount, + ssNone, encryptedPubKey, encryptedPrivKey) + if err != nil { + return err + } + + if updateStartBlock { + return tx.PutStartBlock(bs) + } + + return nil + }) + if err != nil { + return nil, err + } + + // Now that the database has been updated, update the start block in + // memory too if needed. + if updateStartBlock { + m.syncState.startBlock = *bs + } + + // Create a new managed address based on the imported address. + var managedAddr *managedAddress + if !m.watchingOnly { + managedAddr, err = newManagedAddress(m, ImportedAddrAccount, + wif.PrivKey, wif.CompressPubKey) + } else { + pubKey := (*btcec.PublicKey)(&wif.PrivKey.PublicKey) + managedAddr, err = newManagedAddressWithoutPrivKey(m, + ImportedAddrAccount, pubKey, wif.CompressPubKey) + } + if err != nil { + return nil, err + } + managedAddr.imported = true + + // Add the new managed address to the cache of recent addresses and + // return it. + m.addrs[addrKey(managedAddr.Address().ScriptAddress())] = managedAddr + return managedAddr, nil +} + +// ImportScript imports a user-provided script into the address manager. The +// imported script will act as a pay-to-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 (m *Manager) ImportScript(script []byte, bs *BlockStamp) (ManagedScriptAddress, error) { + m.mtx.Lock() + defer m.mtx.Unlock() + + // The manager must be unlocked to encrypt the imported script. + if m.locked && !m.watchingOnly { + return nil, managerError(ErrLocked, errLocked, nil) + } + + // Prevent duplicates. + scriptHash := btcutil.Hash160(script) + alreadyExists, err := m.existsAddress(scriptHash) + if err != nil { + return nil, err + } + if alreadyExists { + str := fmt.Sprintf("address for script hash %x already exists", + scriptHash) + return nil, managerError(ErrDuplicate, str, nil) + } + + // Encrypt the script hash using the crypto public key so it is + // accessible when the address manager is locked or watching-only. + encryptedHash, err := m.cryptoKeyPub.Encrypt(scriptHash) + if err != nil { + str := fmt.Sprintf("failed to encrypt script hash %x", + scriptHash) + 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 !m.watchingOnly { + encryptedScript, err = m.cryptoKeyScript.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 + // is before the current one. + updateStartBlock := false + if bs.Height < m.syncState.startBlock.Height { + updateStartBlock = true + } + + // Save the new imported address to the db and update start block (if + // needed) in a single transaction. + err = m.db.Update(func(tx *managerTx) error { + err := tx.PutScriptAddress(scriptHash, ImportedAddrAccount, + ssNone, encryptedHash, encryptedScript) + if err != nil { + return err + } + + if updateStartBlock { + return tx.PutStartBlock(bs) + } + + return nil + }) + if err != nil { + return nil, err + } + + // Now that the database has been updated, update the start block in + // memory too if needed. + if updateStartBlock { + m.syncState.startBlock = *bs + } + + // Create a new managed address based on the imported script. Also, + // 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(m, ImportedAddrAccount, scriptHash, + encryptedScript) + if err != nil { + return nil, err + } + if !m.watchingOnly { + scriptAddr.scriptCT = make([]byte, len(script)) + copy(scriptAddr.scriptCT, script) + } + + // Add the new managed address to the cache of recent addresses and + // return it. + m.addrs[addrKey(scriptHash)] = scriptAddr + return scriptAddr, nil +} + +// IsLocked returns whether or not the address managed is locked. When it is +// unlocked, the decryption key needed to decrypt private keys used for signing +// is in memory. +func (m *Manager) IsLocked() bool { + m.mtx.RLock() + defer m.mtx.RUnlock() + + return m.locked +} + +// Lock performs a best try effort to remove and zero all secret keys associated +// with the address manager. +// +// This function will return an error if invoked on a watching-only address +// manager. +func (m *Manager) Lock() error { + // A watching-only address manager can't be locked. + if m.watchingOnly { + return managerError(ErrWatchingOnly, errWatchingOnly, nil) + } + + m.mtx.Lock() + defer m.mtx.Unlock() + + // Error on attempt to lock an already locked manager. + if m.locked { + return managerError(ErrLocked, errLocked, nil) + } + + m.lock() + return nil +} + +// Unlock derives the master private key from the specified passphrase. An +// invalid passphrase will return an error. Otherwise, the derived secret key +// is stored in memory until the address manager is locked. Any failures that +// occur during this function will result in the address manager being locked, +// even if it was already unlocked prior to calling this function. +// +// This function will return an error if invoked on a watching-only address +// manager. +func (m *Manager) Unlock(passphrase []byte) error { + // A watching-only address manager can't be unlocked. + if m.watchingOnly { + return managerError(ErrWatchingOnly, errWatchingOnly, nil) + } + + m.mtx.Lock() + defer m.mtx.Unlock() + + // Derive the master private key using the provided passphrase. + if err := m.masterKeyPriv.DeriveKey(&passphrase); err != nil { + m.lock() + if err == snacl.ErrInvalidPassword { + str := "invalid passphrase for master private key" + return managerError(ErrWrongPassphrase, str, nil) + } + + str := "failed to derive master private key" + return managerError(ErrCrypto, str, err) + } + + // Use the master private key to decrypt the crypto private key. + decryptedKey, err := m.masterKeyPriv.Decrypt(m.cryptoKeyPrivEncrypted) + if err != nil { + m.lock() + str := "failed to decrypt crypto private key" + return managerError(ErrCrypto, str, err) + } + copy(m.cryptoKeyPriv[:], decryptedKey) + zero(decryptedKey) + + // Use the crypto private key to decrypt all of the account private + // extended keys. + for account, acctInfo := range m.acctInfo { + decrypted, err := m.cryptoKeyPriv.Decrypt(acctInfo.acctKeyEncrypted) + if err != nil { + m.lock() + str := fmt.Sprintf("failed to decrypt account %d "+ + "private key", account) + return managerError(ErrCrypto, str, err) + } + + acctKeyPriv, err := hdkeychain.NewKeyFromString(string(decrypted)) + zero(decrypted) + if err != nil { + m.lock() + str := fmt.Sprintf("failed to regenerate account %d "+ + "extended key", account) + return managerError(ErrKeyChain, str, err) + } + acctInfo.acctKeyPriv = acctKeyPriv + } + + // Derive any private keys that are pending due to them being created + // while the address manager was locked. + for _, info := range m.deriveOnUnlock { + addressKey, err := m.deriveKeyFromPath(info.managedAddr.account, + info.branch, info.index, true) + if err != nil { + m.lock() + return err + } + + // It's ok to ignore the error here since it can only fail if + // the extended key is not private, however it was just derived + // as a private key. + privKey, _ := addressKey.ECPrivKey() + addressKey.Zero() + + privKeyBytes := privKey.Serialize() + privKeyEncrypted, err := m.cryptoKeyPriv.Encrypt(privKeyBytes) + zeroBigInt(privKey.D) + if err != nil { + m.lock() + str := fmt.Sprintf("failed to encrypt private key for "+ + "address %s", info.managedAddr.Address()) + return managerError(ErrCrypto, str, err) + } + info.managedAddr.privKeyEncrypted = privKeyEncrypted + info.managedAddr.privKeyCT = privKeyBytes + } + + m.locked = false + return nil +} + +// Net returns the network parameters for this address manager. +func (m *Manager) Net() *btcnet.Params { + // NOTE: No need for mutex here since the net field does not change + // after the manager instance is created. + + return m.net +} + +// nextAddresses returns the specified number of next chained address from the +// branch indicated by the internal flag. +// +// This function MUST be called with the manager lock held for writes. +func (m *Manager) nextAddresses(account uint32, numAddresses uint32, internal bool) ([]ManagedAddress, error) { + // The next address can only be generated for accounts that have already + // been created. + acctInfo, err := m.loadAccountInfo(account) + if err != nil { + return nil, err + } + + // Choose the account key to used based on whether the address manager + // is locked. + acctKey := acctInfo.acctKeyPub + if !m.locked { + acctKey = acctInfo.acctKeyPriv + } + + // Choose the branch key and index depending on whether or not this + // is an internal address. + branchNum, nextIndex := externalBranch, acctInfo.nextExternalIndex + if internal { + branchNum = internalBranch + nextIndex = acctInfo.nextInternalIndex + } + + // Ensure the requested number of addresses doesn't exceed the maximum + // allowed for this account. + if numAddresses > MaxAddressesPerAccount || nextIndex+numAddresses > + MaxAddressesPerAccount { + str := fmt.Sprintf("%d new addresses would exceed the maximum "+ + "allowed number of addresses per account of %d", + numAddresses, MaxAddressesPerAccount) + return nil, managerError(ErrTooManyAddresses, str, nil) + } + + // Derive the appropriate branch key and ensure it is zeroed when done. + branchKey, err := acctKey.Child(branchNum) + if err != nil { + str := fmt.Sprintf("failed to derive extended key branch %d", + branchNum) + return nil, managerError(ErrKeyChain, str, err) + } + defer branchKey.Zero() // Ensure branch key is zeroed when done. + + // Create the requested number of addresses and keep track of the index + // with each one. + addressInfo := make([]*unlockDeriveInfo, 0, numAddresses) + for i := uint32(0); i < numAddresses; i++ { + // There is an extremely small chance that a particular child is + // invalid, so use a loop to derive the next valid child. + var nextKey *hdkeychain.ExtendedKey + for { + // Derive the next child in the external chain branch. + key, err := branchKey.Child(nextIndex) + if err != nil { + // When this particular child is invalid, skip to the + // next index. + if err == hdkeychain.ErrInvalidChild { + nextIndex++ + continue + } + + str := fmt.Sprintf("failed to generate child %d", + nextIndex) + return nil, managerError(ErrKeyChain, str, err) + } + key.SetNet(m.net) + + nextIndex++ + nextKey = key + break + } + + // Create a new managed address based on the public or private + // key depending on whether the generated key is private. Also, + // zero the next key after creating the managed address from it. + managedAddr, err := newManagedAddressFromExtKey(m, account, nextKey) + nextKey.Zero() + if err != nil { + return nil, err + } + if internal { + managedAddr.internal = true + } + info := unlockDeriveInfo{ + managedAddr: managedAddr, + branch: branchNum, + index: nextIndex - 1, + } + addressInfo = append(addressInfo, &info) + } + + // Now that all addresses have been successfully generated, update the + // database in a single transaction. + err = m.db.Update(func(tx *managerTx) error { + for _, info := range addressInfo { + ma := info.managedAddr + addressID := ma.Address().ScriptAddress() + err := tx.PutChainedAddress(addressID, account, + ssFull, info.branch, info.index) + if err != nil { + return err + } + } + + return nil + }) + if err != nil { + return nil, err + } + + // Finally update the next address tracking and add the addresses to the + // cache after the newly generated addresses have been successfully + // added to the db. + managedAddresses := make([]ManagedAddress, 0, len(addressInfo)) + for _, info := range addressInfo { + ma := info.managedAddr + m.addrs[addrKey(ma.Address().ScriptAddress())] = ma + + // Add the new managed address to the list of addresses that + // need their private keys derived when the address manager is + // next unlocked. + if m.locked && !m.watchingOnly { + m.deriveOnUnlock = append(m.deriveOnUnlock, info) + } + + managedAddresses = append(managedAddresses, ma) + } + + // Set the last address and next address for tracking. + ma := addressInfo[len(addressInfo)-1].managedAddr + if internal { + acctInfo.nextInternalIndex = nextIndex + acctInfo.lastInternalAddr = ma + } else { + acctInfo.nextExternalIndex = nextIndex + acctInfo.lastExternalAddr = ma + } + + return managedAddresses, nil +} + +// NextExternalAddresses returns the specified number of next chained addresses +// that are intended for external use from the address manager. +func (m *Manager) NextExternalAddresses(account uint32, numAddresses uint32) ([]ManagedAddress, error) { + // Enforce maximum account number. + if account > MaxAccountNum { + err := managerError(ErrAccountNumTooHigh, errAcctTooHigh, nil) + return nil, err + } + + m.mtx.Lock() + defer m.mtx.Unlock() + + return m.nextAddresses(account, numAddresses, false) +} + +// NextInternalAddresses returns the specified number of next chained addresses +// that are intended for internal use such as change from the address manager. +func (m *Manager) NextInternalAddresses(account uint32, numAddresses uint32) ([]ManagedAddress, error) { + // Enforce maximum account number. + if account > MaxAccountNum { + err := managerError(ErrAccountNumTooHigh, errAcctTooHigh, nil) + return nil, err + } + + m.mtx.Lock() + defer m.mtx.Unlock() + + return m.nextAddresses(account, numAddresses, true) +} + +// LastExternalAddress returns the most recently requested chained external +// address from calling NextExternalAddress for the given account. The first +// external address for the account will be returned if none have been +// previously requested. +// +// This function will return an error if the provided account number is greater +// than the MaxAccountNum constant or there is no account information for the +// passed account. Any other errors returned are generally unexpected. +func (m *Manager) LastExternalAddress(account uint32) (ManagedAddress, error) { + // Enforce maximum account number. + if account > MaxAccountNum { + err := managerError(ErrAccountNumTooHigh, errAcctTooHigh, nil) + return nil, err + } + + m.mtx.Lock() + defer m.mtx.Unlock() + + // Load account information for the passed account. It is typically + // cached, but if not it will be loaded from the database. + acctInfo, err := m.loadAccountInfo(account) + if err != nil { + return nil, err + } + + return acctInfo.lastExternalAddr, nil +} + +// LastInternalAddress returns the most recently requested chained internal +// address from calling NextInternalAddress for the given account. The first +// internal address for the account will be returned if none have been +// previously requested. +// +// This function will return an error if the provided account number is greater +// than the MaxAccountNum constant or there is no account information for the +// passed account. Any other errors returned are generally unexpected. +func (m *Manager) LastInternalAddress(account uint32) (ManagedAddress, error) { + // Enforce maximum account number. + if account > MaxAccountNum { + err := managerError(ErrAccountNumTooHigh, errAcctTooHigh, nil) + return nil, err + } + + m.mtx.Lock() + defer m.mtx.Unlock() + + // Load account information for the passed account. It is typically + // cached, but if not it will be loaded from the database. + acctInfo, err := m.loadAccountInfo(account) + if err != nil { + return nil, err + } + + return acctInfo.lastInternalAddr, nil +} + +// AllActiveAddresses returns a slice of all addresses stored in the manager. +func (m *Manager) AllActiveAddresses() ([]btcutil.Address, error) { + m.mtx.Lock() + defer m.mtx.Unlock() + + // Load the raw address information from the database. + var rowInterfaces []interface{} + err := m.db.View(func(tx *managerTx) error { + var err error + rowInterfaces, err = tx.FetchAllAddresses() + return err + }) + if err != nil { + return nil, err + } + + addrs := make([]btcutil.Address, 0, len(rowInterfaces)) + for _, rowInterface := range rowInterfaces { + // Create a new managed address for the specific type of address + // based on type. + managedAddr, err := m.rowInterfaceToManaged(rowInterface) + if err != nil { + return nil, err + } + + addrs = append(addrs, managedAddr.Address()) + } + + return addrs, nil +} + +// newManager returns a new locked address manager with the given parameters. +func newManager(db *managerDB, net *btcnet.Params, masterKeyPub *snacl.SecretKey, + masterKeyPriv *snacl.SecretKey, cryptoKeyPub *snacl.CryptoKey, + cryptoKeyPrivEncrypted, cryptoKeyScriptEncrypted []byte, + syncInfo *syncState) *Manager { + + return &Manager{ + db: db, + net: net, + addrs: make(map[addrKey]ManagedAddress), + syncState: *syncInfo, + locked: true, + acctInfo: make(map[uint32]*accountInfo), + masterKeyPub: masterKeyPub, + masterKeyPriv: masterKeyPriv, + cryptoKeyPub: cryptoKeyPub, + cryptoKeyPrivEncrypted: cryptoKeyPrivEncrypted, + cryptoKeyPriv: &snacl.CryptoKey{}, + cryptoKeyScriptEncrypted: cryptoKeyScriptEncrypted, + cryptoKeyScript: &snacl.CryptoKey{}, + } +} + +// filesExists reports whether the named file or directory exists. +func fileExists(name string) bool { + if _, err := os.Stat(name); err != nil { + if os.IsNotExist(err) { + return false + } + } + return true +} + +// deriveAccountKey derives the extended key for an account according to the +// hierarchy described by BIP0044 given the master node. +// +// In particular this is the hierarchical deterministic extended key path: +// m/44'/'/' +func deriveAccountKey(masterNode *hdkeychain.ExtendedKey, coinType uint32, + account uint32) (*hdkeychain.ExtendedKey, error) { + + // Enforce maximum coin type. + if coinType > maxCoinType { + err := managerError(ErrCoinTypeTooHigh, errCoinTypeTooHigh, nil) + return nil, err + } + + // Enforce maximum account number. + if account > MaxAccountNum { + err := managerError(ErrAccountNumTooHigh, errAcctTooHigh, nil) + return nil, err + } + + // The hierarchy described by BIP0043 is: + // m/'/* + // This is further extended by BIP0044 to: + // m/44'/'/'//
+ // + // The branch is 0 for external addresses and 1 for internal addresses. + + // Derive the purpose key as a child of the master node. + purpose, err := masterNode.Child(44 + hdkeychain.HardenedKeyStart) + if err != nil { + return nil, err + } + + // Derive the coin type key as a child of the purpose key. + coinTypeKey, err := purpose.Child(coinType + hdkeychain.HardenedKeyStart) + if err != nil { + return nil, err + } + + // Derive the account key as a child of the coin type key. + return coinTypeKey.Child(account + hdkeychain.HardenedKeyStart) +} + +// checkBranchKeys ensures deriving the extended keys for the internal and +// external branches given an account key does not result in an invalid child +// error which means the chosen seed is not usable. This conforms to the +// hierarchy described by BIP0044 so long as the account key is already derived +// accordingly. +// +// In particular this is the hierarchical deterministic extended key path: +// m/44'/'/'/ +// +// The branch is 0 for external addresses and 1 for internal addresses. +func checkBranchKeys(acctKey *hdkeychain.ExtendedKey) error { + // Derive the external branch as the first child of the account key. + if _, err := acctKey.Child(externalBranch); err != nil { + return err + } + + // Derive the external branch as the second child of the account key. + _, err := acctKey.Child(internalBranch) + return err +} + +// loadManager returns a new address manager that results from loading it from +// the passed opened database. The public passphrase is required to decrypt the +// public keys. +func loadManager(db *managerDB, pubPassphrase []byte, net *btcnet.Params) (*Manager, error) { + // Perform all database lookups in a read-only view. + var watchingOnly bool + var masterKeyPubParams, masterKeyPrivParams []byte + var cryptoKeyPubEnc, cryptoKeyPrivEnc, cryptoKeyScriptEnc []byte + var syncedTo, startBlock *BlockStamp + var recentHeight int32 + var recentHashes []btcwire.ShaHash + err := db.View(func(tx *managerTx) error { + // Load whether or not the manager is watching-only from the db. + var err error + watchingOnly, err = tx.FetchWatchingOnly() + if err != nil { + return err + } + + // Load the master key params from the db. + masterKeyPubParams, masterKeyPrivParams, err = + tx.FetchMasterKeyParams() + if err != nil { + return err + } + + // Load the crypto keys from the db. + cryptoKeyPubEnc, cryptoKeyPrivEnc, cryptoKeyScriptEnc, err = + tx.FetchCryptoKeys() + if err != nil { + return err + } + + // Load the sync state from the db. + syncedTo, err = tx.FetchSyncedTo() + if err != nil { + return err + } + startBlock, err = tx.FetchStartBlock() + if err != nil { + return err + } + + recentHeight, recentHashes, err = tx.FetchRecentBlocks() + return err + }) + if err != nil { + return nil, err + } + + // When not a watching-only manager, set the master private key params, + // but don't derive it now since the manager starts off locked. + var masterKeyPriv snacl.SecretKey + if !watchingOnly { + err := masterKeyPriv.Unmarshal(masterKeyPrivParams) + if err != nil { + str := "failed to unmarshal master private key" + return nil, managerError(ErrCrypto, str, err) + } + } + + // Derive the master public key using the serialized params and provided + // passphrase. + var masterKeyPub snacl.SecretKey + if err := masterKeyPub.Unmarshal(masterKeyPubParams); err != nil { + str := "failed to unmarshal master public key" + return nil, managerError(ErrCrypto, str, err) + } + if err := masterKeyPub.DeriveKey(&pubPassphrase); err != nil { + str := "invalid passphrase for master public key" + return nil, managerError(ErrWrongPassphrase, str, nil) + } + + // Use the master public key to decrypt the crypto public key. + var cryptoKeyPub snacl.CryptoKey + cryptoKeyPubCT, err := masterKeyPub.Decrypt(cryptoKeyPubEnc) + if err != nil { + str := "failed to decrypt crypto public key" + return nil, managerError(ErrCrypto, str, err) + } + copy(cryptoKeyPub[:], cryptoKeyPubCT) + zero(cryptoKeyPubCT) + + // Create the sync state struct. + syncInfo := newSyncState(startBlock, syncedTo, recentHeight, recentHashes) + + // Create new address manager with the given parameters. Also, override + // the defaults for the additional fields which are not specified in the + // call to new with the values loaded from the database. + mgr := newManager(db, net, &masterKeyPub, &masterKeyPriv, &cryptoKeyPub, + cryptoKeyPrivEnc, cryptoKeyScriptEnc, syncInfo) + mgr.watchingOnly = watchingOnly + return mgr, nil +} + +// Open loads an existing address manager from the given database path. The +// public passphrase is required to decrypt the public keys used to protect the +// public information such as addresses. This is important since access to +// BIP0032 extended keys means it is possible to generate all future addresses. +// +// A ManagerError with an error code of ErrNoExist will be returned if the +// passed database does not exist. +func Open(dbPath string, pubPassphrase []byte, net *btcnet.Params) (*Manager, error) { + // Return an error if the specified database does not exist. + if !fileExists(dbPath) { + str := "the specified address manager does not exist" + return nil, managerError(ErrNoExist, str, nil) + } + + db, err := openOrCreateDB(dbPath) + if err != nil { + return nil, err + } + + return loadManager(db, pubPassphrase, net) +} + +// Create returns a new locked address manager at the given database path. The +// seed must conform to the standards described in hdkeychain.NewMaster and will +// be used to create the master root node from which all hierarchical +// deterministic addresses are derived. This allows all chained addresses in +// the address manager to be recovered by using the same seed. +// +// All private and public keys and information are protected by secret keys +// derived from the provided private and public passphrases. The public +// passphrase is required on subsequent opens of the address manager, and the +// private passphrase is required to unlock the address manager in order to gain +// access to any private keys and information. +// +// A ManagerError with an error code of ErrAlreadyExists will be returned if the +// passed database already exists. +func Create(dbPath string, seed, pubPassphrase, privPassphrase []byte, net *btcnet.Params) (*Manager, error) { + // Return an error if the specified database already exists. + if fileExists(dbPath) { + return nil, managerError(ErrAlreadyExists, errAlreadyExists, nil) + } + + db, err := openOrCreateDB(dbPath) + if err != nil { + return nil, err + } + + // Generate the BIP0044 HD key structure to ensure the provided seed + // can generate the required structure with no issues. + + // Derive the master extended key from the seed. + root, err := hdkeychain.NewMaster(seed) + if err != nil { + str := "failed to derive master extended key" + return nil, managerError(ErrKeyChain, str, err) + } + + // Derive the account key for the first account according to BIP0044. + acctKeyPriv, err := deriveAccountKey(root, net.HDCoinType, 0) + if err != nil { + // The seed is unusable if the any of the children in the + // required hierarchy can't be derived due to invalid child. + if err == hdkeychain.ErrInvalidChild { + str := "the provided seed is unusable" + return nil, managerError(ErrKeyChain, str, + hdkeychain.ErrUnusableSeed) + } + + return nil, err + } + + // Ensure the branch keys can be derived for the provided seed according + // to BIP0044. + if err := checkBranchKeys(acctKeyPriv); err != nil { + // The seed is unusable if the any of the children in the + // required hierarchy can't be derived due to invalid child. + if err == hdkeychain.ErrInvalidChild { + str := "the provided seed is unusable" + return nil, managerError(ErrKeyChain, str, + hdkeychain.ErrUnusableSeed) + } + + return nil, err + } + + // The address manager needs the public extended key for the account. + acctKeyPub, err := acctKeyPriv.Neuter() + if err != nil { + str := "failed to convert private key for account 0" + return nil, managerError(ErrKeyChain, str, err) + } + + // Generate new master keys. These master keys are used to protect the + // crypto keys that will be generated next. + masterKeyPub, err := newSecretKey(&pubPassphrase) + if err != nil { + str := "failed to master public key" + return nil, managerError(ErrCrypto, str, err) + } + masterKeyPriv, err := newSecretKey(&privPassphrase) + if err != nil { + str := "failed to master private key" + return nil, managerError(ErrCrypto, str, err) + } + + // Generate new crypto public, private, and script keys. These keys are + // used to protect the actual public and private data such as addresses, + // extended keys, and scripts. + cryptoKeyPub, err := snacl.GenerateCryptoKey() + if err != nil { + str := "failed to generate crypto public key" + return nil, managerError(ErrCrypto, str, err) + } + cryptoKeyPriv, err := snacl.GenerateCryptoKey() + if err != nil { + str := "failed to generate crypto private key" + return nil, managerError(ErrCrypto, str, err) + } + cryptoKeyScript, err := snacl.GenerateCryptoKey() + if err != nil { + str := "failed to generate crypto script key" + return nil, managerError(ErrCrypto, str, err) + } + + // Encrypt the crypto keys with the associated master keys. + cryptoKeyPubEnc, err := masterKeyPub.Encrypt(cryptoKeyPub[:]) + if err != nil { + str := "failed to encrypt crypto public key" + return nil, managerError(ErrCrypto, str, err) + } + cryptoKeyPrivEnc, err := masterKeyPriv.Encrypt(cryptoKeyPriv[:]) + if err != nil { + str := "failed to encrypt crypto private key" + return nil, managerError(ErrCrypto, str, err) + } + cryptoKeyScriptEnc, err := masterKeyPriv.Encrypt(cryptoKeyScript[:]) + if err != nil { + str := "failed to encrypt crypto script key" + return nil, managerError(ErrCrypto, str, err) + } + + // Encrypt the default account keys with the associated crypto keys. + acctPubEnc, err := cryptoKeyPub.Encrypt([]byte(acctKeyPub.String())) + if err != nil { + str := "failed to encrypt public key for account 0" + return nil, managerError(ErrCrypto, str, err) + } + acctPrivEnc, err := cryptoKeyPriv.Encrypt([]byte(acctKeyPriv.String())) + if err != nil { + str := "failed to encrypt private key for account 0" + return nil, managerError(ErrCrypto, str, err) + } + + // Use the genesis block for the passed network as the created at block + // for the defaut. + createdAt := &BlockStamp{Hash: *net.GenesisHash, Height: 0} + + // Create the initial sync state. + recentHashes := []btcwire.ShaHash{createdAt.Hash} + recentHeight := createdAt.Height + syncInfo := newSyncState(createdAt, createdAt, recentHeight, recentHashes) + + // Perform all database updates in a single transaction. + err = db.Update(func(tx *managerTx) error { + // Save the master key params to the database. + pubParams := masterKeyPub.Marshal() + privParams := masterKeyPriv.Marshal() + err = tx.PutMasterKeyParams(pubParams, privParams) + if err != nil { + return err + } + + // Save the encrypted crypto keys to the database. + err = tx.PutCryptoKeys(cryptoKeyPubEnc, cryptoKeyPrivEnc, + cryptoKeyScriptEnc) + if err != nil { + return err + } + + // Save the fact this is not a watching-only address manager to + // the database. + err = tx.PutWatchingOnly(false) + if err != nil { + return err + } + + // Save the initial synced to state. + err = tx.PutSyncedTo(&syncInfo.syncedTo) + if err != nil { + return err + } + err = tx.PutStartBlock(&syncInfo.startBlock) + if err != nil { + return err + } + + // Save the initial recent blocks state. + err = tx.PutRecentBlocks(recentHeight, recentHashes) + if err != nil { + return err + } + + // Save the information for the default account to the database. + err = tx.PutAccountInfo(defaultAccountNum, acctPubEnc, + acctPrivEnc, 0, 0, "") + if err != nil { + return err + } + + return tx.PutNumAccounts(1) + }) + if err != nil { + return nil, err + } + + // The new address manager is locked by default, so clear the master, + // crypto private, and crypto script keys from memory. + masterKeyPriv.Zero() + cryptoKeyPriv.Zero() + cryptoKeyScript.Zero() + return newManager(db, net, masterKeyPub, masterKeyPriv, cryptoKeyPub, + cryptoKeyPrivEnc, cryptoKeyScriptEnc, syncInfo), nil +} diff --git a/waddrmgr/manager_test.go b/waddrmgr/manager_test.go new file mode 100644 index 0000000..d82b177 --- /dev/null +++ b/waddrmgr/manager_test.go @@ -0,0 +1,1500 @@ +/* + * Copyright (c) 2014 Conformal Systems LLC + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package waddrmgr_test + +import ( + "encoding/hex" + "fmt" + "os" + "reflect" + "testing" + + "github.com/conformal/btcnet" + "github.com/conformal/btcutil" + "github.com/conformal/btcwallet/waddrmgr" + "github.com/conformal/btcwire" +) + +// newShaHash converts the passed big-endian hex string into a btcwire.ShaHash. +// It only differs from the one available in btcwire in that it panics on an +// error since it will only (and must only) be called with hard-coded, and +// therefore known good, hashes. +func newShaHash(hexStr string) *btcwire.ShaHash { + sha, err := btcwire.NewShaHashFromStr(hexStr) + if err != nil { + panic(err) + } + return sha +} + +// testContext is used to store context information about a running test which +// is passed into helper functions. The useSpends field indicates whether or +// not the spend data should be empty or figure it out based on the specific +// test blocks provided. This is needed because the first loop where the blocks +// are inserted, the tests are running against the latest block and therefore +// none of the outputs can be spent yet. However, on subsequent runs, all +// blocks have been inserted and therefore some of the transaction outputs are +// spent. +type testContext struct { + t *testing.T + manager *waddrmgr.Manager + account uint32 + create bool + unlocked bool + watchingOnly bool +} + +// 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 +} + +// testNamePrefix is a helper to return a prefix to show for test errors based +// on the state of the test context. +func testNamePrefix(tc *testContext) string { + prefix := "Open " + if tc.create { + prefix = "Create " + } + + return prefix + fmt.Sprintf("account #%d", tc.account) +} + +// testManagedPubKeyAddress ensures the data returned by all exported functions +// provided by the passed managed p ublic key address matches the corresponding +// fields in the provided expected address. +// +// When the test context indicates the manager is unlocked, the private data +// will also be tested, otherwise, the functions which deal with private data +// are checked to ensure they return the correct error. +func testManagedPubKeyAddress(tc *testContext, prefix string, gotAddr waddrmgr.ManagedPubKeyAddress, wantAddr *expectedAddr) bool { + // Ensure pubkey is the expected value for the managed address. + var gpubBytes []byte + if gotAddr.Compressed() { + gpubBytes = gotAddr.PubKey().SerializeCompressed() + } else { + gpubBytes = gotAddr.PubKey().SerializeUncompressed() + } + if !reflect.DeepEqual(gpubBytes, wantAddr.pubKey) { + tc.t.Errorf("%s PubKey: unexpected public key - got %x, want "+ + "%x", prefix, gpubBytes, wantAddr.pubKey) + return false + } + + // Ensure exported pubkey string is the expected value for the managed + // address. + gpubHex := gotAddr.ExportPubKey() + wantPubHex := hex.EncodeToString(wantAddr.pubKey) + if gpubHex != wantPubHex { + tc.t.Errorf("%s ExportPubKey: unexpected public key - got %s, "+ + "want %s", prefix, gpubHex, wantPubHex) + return false + } + + // Ensure private key is the expected value for the managed address. + // Since this is only available when the manager is unlocked, also check + // for the expected error when the manager is locked. + gotPrivKey, err := gotAddr.PrivKey() + switch { + case tc.watchingOnly: + // Confirm expected watching-only error. + testName := fmt.Sprintf("%s PrivKey", prefix) + if !checkManagerError(tc.t, testName, err, waddrmgr.ErrWatchingOnly) { + return false + } + case tc.unlocked: + if err != nil { + tc.t.Errorf("%s PrivKey: unexpected error - got %v", + prefix, err) + return false + } + gpriv := gotPrivKey.Serialize() + if !reflect.DeepEqual(gpriv, wantAddr.privKey) { + tc.t.Errorf("%s PrivKey: unexpected private key - "+ + "got %x, want %x", prefix, gpriv, wantAddr.privKey) + return false + } + default: + // Confirm expected locked error. + testName := fmt.Sprintf("%s PrivKey", prefix) + if !checkManagerError(tc.t, testName, err, waddrmgr.ErrLocked) { + return false + } + } + + // Ensure exported private key in Wallet Import Format (WIF) is the + // expected value for the managed address. Since this is only available + // when the manager is unlocked, also check for the expected error when + // the manager is locked. + gotWIF, err := gotAddr.ExportPrivKey() + switch { + case tc.watchingOnly: + // Confirm expected watching-only error. + testName := fmt.Sprintf("%s ExportPrivKey", prefix) + if !checkManagerError(tc.t, testName, err, waddrmgr.ErrWatchingOnly) { + return false + } + case tc.unlocked: + if err != nil { + tc.t.Errorf("%s ExportPrivKey: unexpected error - "+ + "got %v", prefix, err) + return false + } + if gotWIF.String() != wantAddr.privKeyWIF { + tc.t.Errorf("%s ExportPrivKey: unexpected WIF - got "+ + "%v, want %v", prefix, gotWIF.String(), + wantAddr.privKeyWIF) + return false + } + default: + // Confirm expected locked error. + testName := fmt.Sprintf("%s ExportPrivKey", prefix) + if !checkManagerError(tc.t, testName, err, waddrmgr.ErrLocked) { + return false + } + } + + return true +} + +// testManagedScriptAddress ensures the data returned by all exported functions +// provided by the passed managed script address matches the corresponding +// fields in the provided expected address. +// +// When the test context indicates the manager is unlocked, the private data +// will also be tested, otherwise, the functions which deal with private data +// are checked to ensure they return the correct error. +func testManagedScriptAddress(tc *testContext, prefix string, gotAddr waddrmgr.ManagedScriptAddress, wantAddr *expectedAddr) bool { + // Ensure script is the expected value for the managed address. + // Ensure script is the expected value for the managed address. Since + // this is only available when the manager is unlocked, also check for + // the expected error when the manager is locked. + gotScript, err := gotAddr.Script() + switch { + case tc.watchingOnly: + // Confirm expected watching-only error. + testName := fmt.Sprintf("%s Script", prefix) + if !checkManagerError(tc.t, testName, err, waddrmgr.ErrWatchingOnly) { + return false + } + case tc.unlocked: + if err != nil { + tc.t.Errorf("%s Script: unexpected error - got %v", + prefix, err) + return false + } + if !reflect.DeepEqual(gotScript, wantAddr.script) { + tc.t.Errorf("%s Script: unexpected script - got %x, "+ + "want %x", prefix, gotScript, wantAddr.script) + return false + } + default: + // Confirm expected locked error. + testName := fmt.Sprintf("%s Script", prefix) + if !checkManagerError(tc.t, testName, err, waddrmgr.ErrLocked) { + return false + } + } + + return true +} + +// testAddress ensures the data returned by all exported functions provided by +// the passed managed address matches the corresponding fields in the provided +// expected address. It also type asserts the managed address to determine its +// specific type and calls the corresponding testing functions accordingly. +// +// When the test context indicates the manager is unlocked, the private data +// will also be tested, otherwise, the functions which deal with private data +// are checked to ensure they return the correct error. +func testAddress(tc *testContext, prefix string, gotAddr waddrmgr.ManagedAddress, wantAddr *expectedAddr) bool { + if gotAddr.Account() != tc.account { + tc.t.Errorf("ManagedAddress.Account: unexpected account - got "+ + "%d, want %d", gotAddr.Account(), tc.account) + return false + } + + if gotAddr.Address().EncodeAddress() != wantAddr.address { + tc.t.Errorf("%s EncodeAddress: unexpected address - got %s, "+ + "want %s", prefix, gotAddr.Address().EncodeAddress(), + wantAddr.address) + return false + } + + if !reflect.DeepEqual(gotAddr.AddrHash(), wantAddr.addressHash) { + tc.t.Errorf("%s AddrHash: unexpected address hash - got %x, "+ + "want %x", prefix, gotAddr.AddrHash(), + wantAddr.addressHash) + return false + } + + if gotAddr.Internal() != wantAddr.internal { + tc.t.Errorf("%s Internal: unexpected internal flag - got %v, "+ + "want %v", prefix, gotAddr.Internal(), wantAddr.internal) + return false + } + + if gotAddr.Compressed() != wantAddr.compressed { + tc.t.Errorf("%s Compressed: unexpected compressed flag - got "+ + "%v, want %v", prefix, gotAddr.Compressed(), + wantAddr.compressed) + return false + } + + if gotAddr.Imported() != wantAddr.imported { + tc.t.Errorf("%s Imported: unexpected imported flag - got %v, "+ + "want %v", prefix, gotAddr.Imported(), wantAddr.imported) + return false + } + + switch addr := gotAddr.(type) { + case waddrmgr.ManagedPubKeyAddress: + if !testManagedPubKeyAddress(tc, prefix, addr, wantAddr) { + return false + } + + case waddrmgr.ManagedScriptAddress: + if !testManagedScriptAddress(tc, prefix, addr, wantAddr) { + return false + } + } + + return true +} + +// testExternalAddresses tests several facets of external addresses such as +// generating multiple addresses via NextExternalAddresses, ensuring they can be +// retrieved by Address, and that they work properly when the manager is locked +// and unlocked. +func testExternalAddresses(tc *testContext) bool { + // Define the expected addresses. + expectedAddrs := []expectedAddr{ + { + address: "14wtcepMNiEazuN7YosWY8bwD9tcCtxXRB", + addressHash: hexToBytes("2b49ecd0cf72006173e6e95acf416b6735b5f889"), + internal: false, + compressed: true, + imported: false, + pubKey: hexToBytes("02d8f88468c5a2e8e1815faf555f59cbd1979e3dbdf823f80c271b6fb70d2d519b"), + privKey: hexToBytes("c27d6581b92785834b381fa697c4b0ffc4574b495743722e0acb7601b1b68b99"), + privKeyWIF: "L3jmpy54Pc7MLXTN2mL8Xas7BJziwKaUGmgnXXzgGbVRdiAniXZk", + }, + { + address: "1N3D8jy2aQuUsKBsDgZ6ZPTVR9VhHgJYpE", + addressHash: hexToBytes("e6c59a1542138d1bf08f45cd18899557cf56b356"), + internal: false, + compressed: true, + imported: false, + pubKey: hexToBytes("02b9c175b908624f8a8eaac227d0e8c77c0eec327b8c512ad1b8b7a4b5b676971f"), + privKey: hexToBytes("18f3b191019e83878a81557abebb2afda199e31d22e150d8bf4df4561671be6c"), + privKeyWIF: "Kx4DNid19W8sjNFN3uPqQE7UYnCqyEp7unCvdkf2LrVUFpnDtwpB", + }, + { + address: "1VTfwD4iHre2bMrR9qGiJMwoiZGQZ8e6s", + addressHash: hexToBytes("0561e9373986965b647a57a09718e9c050215cfe"), + internal: false, + compressed: true, + imported: false, + pubKey: hexToBytes("0329faddf1254d490d6add49e2b08cf52b561038c72baec0edb3cfacff71ff1021"), + privKey: hexToBytes("ccb8f6305b73136b363644b647f6efc0fd27b6b7d9c11c7e560662ed38db7b34"), + privKeyWIF: "L45fWF6Yd736fDohuB97vwRRLdQQJr3ZGvbokk9ubiT7aNrg7tTn", + }, + { + address: "13TdEj4ehUuYFiSaB47eLVBwM2XhAhrK2J", + addressHash: hexToBytes("1af950be02584ca230b7078cec0cfd38dd71b468"), + internal: false, + compressed: true, + imported: false, + pubKey: hexToBytes("03d738324e2f0ce42e46975d7f8c7117c1670e3d7912b0291aea452add99674774"), + privKey: hexToBytes("d6bc8ff768814fede2adcdb74826bd846924341b3862e3b6e31cdc084e992940"), + privKeyWIF: "L4R8XyxYQyPSpTwj8w96tM86a6j3QA9jbRPj3RA7DVTVWk71ndeP", + }, + { + address: "1LTjSghkBecT59VjEKke331HxVdqcFwUDa", + addressHash: hexToBytes("d578a267a7174c6ba7f76b0ab2397ce0ba0c5c3c"), + internal: false, + compressed: true, + imported: false, + pubKey: hexToBytes("03a917acd5cd5b6f544b43f1921a35677e4d5320e5d2add2056039b4b44fdf905e"), + privKey: hexToBytes("8563ade061110e03aee50695ffc5cb1c06c8310bde0a3674257c853c966968c0"), + privKeyWIF: "L1h16Hunxomww4FrpyQP2iFmWNgG7U1u3awp6Vd3s2uGf7v5VU8c", + }, + } + + prefix := testNamePrefix(tc) + " testExternalAddresses" + var addrs []waddrmgr.ManagedAddress + if tc.create { + prefix := prefix + " NextExternalAddresses" + var err error + addrs, err = tc.manager.NextExternalAddresses(tc.account, 5) + if err != nil { + tc.t.Errorf("%s: unexpected error: %v", prefix, err) + return false + } + if len(addrs) != len(expectedAddrs) { + tc.t.Errorf("%s: unexpected number of addresses - got "+ + "%d, want %d", prefix, len(addrs), + len(expectedAddrs)) + return false + } + } + + // Setup a closure to test the results since the same tests need to be + // repeated with the manager locked and unlocked. + testResults := func() bool { + // Ensure the returned addresses are the expected ones. When + // not in the create phase, there will be no addresses in the + // addrs slice, so this really only runs during the first phase + // of the tests. + for i := 0; i < len(addrs); i++ { + prefix := fmt.Sprintf("%s ExternalAddress #%d", prefix, i) + if !testAddress(tc, prefix, addrs[i], &expectedAddrs[i]) { + return false + } + } + + // Ensure the last external address is the expected one. + leaPrefix := prefix + " LastExternalAddress" + lastAddr, err := tc.manager.LastExternalAddress(tc.account) + if err != nil { + tc.t.Errorf("%s: unexpected error: %v", leaPrefix, err) + return false + } + if !testAddress(tc, leaPrefix, lastAddr, &expectedAddrs[len(expectedAddrs)-1]) { + return false + } + + // Now, use the Address API to retrieve each of the expected new + // addresses and ensure they're accurate. + net := tc.manager.Net() + for i := 0; i < len(expectedAddrs); i++ { + pkHash := expectedAddrs[i].addressHash + utilAddr, err := btcutil.NewAddressPubKeyHash(pkHash, net) + if err != nil { + tc.t.Errorf("%s NewAddressPubKeyHash #%d: "+ + "unexpected error: %v", prefix, i, err) + return false + } + + prefix := fmt.Sprintf("%s Address #%d", prefix, i) + addr, err := tc.manager.Address(utilAddr) + if err != nil { + tc.t.Errorf("%s: unexpected error: %v", prefix, + err) + return false + } + + if !testAddress(tc, prefix, addr, &expectedAddrs[i]) { + return false + } + } + + return true + } + + // Since the manager is locked at this point, the public address + // information is tested and the private functions are checked to ensure + // they return the expected error. + if !testResults() { + return false + } + + // Everything after this point involves retesting with an unlocked + // address manager which is not possible for watching-only mode, so + // just exit now in that case. + if tc.watchingOnly { + return true + } + + // Unlock the manager and retest all of the addresses to ensure the + // private information is valid as well. + if err := tc.manager.Unlock(privPassphrase); err != nil { + tc.t.Errorf("Unlock: unexpected error: %v", err) + return false + } + tc.unlocked = true + if !testResults() { + return false + } + + // Relock the manager for future tests. + if err := tc.manager.Lock(); err != nil { + tc.t.Errorf("Lock: unexpected error: %v", err) + return false + } + tc.unlocked = false + + return true +} + +// testInternalAddresses tests several facets of internal addresses such as +// generating multiple addresses via NextInternalAddresses, ensuring they can be +// retrieved by Address, and that they work properly when the manager is locked +// and unlocked. +func testInternalAddresses(tc *testContext) bool { + // Define the expected addresses. + expectedAddrs := []expectedAddr{ + { + address: "15HNivzKhsLaMs1qRdQN1ifoJYUnJ2xW9z", + addressHash: hexToBytes("2ef94abb9ee8f785d087c3ec8d6ee467e92d0d0a"), + internal: true, + compressed: true, + imported: false, + pubKey: hexToBytes("020a1290b997c0a234a95213962e7edcb761c7360f0230f698a1a3e71c37047bb0"), + privKey: hexToBytes("fe4f855fcf059ec6ddf7b25f63b19aa49c771d1fcb9850b68ae3d65e20657a60"), + privKeyWIF: "L5k4HivqXvohxBMpuwD38iUgi6uewffwZny91ZNYfM39RXH2x3QR", + }, + { + address: "1LJpGrAP1vWHuvfHqmUutQqFVYca2qwxhy", + addressHash: hexToBytes("d3c8ec46891f599bfeaa4c25918bfb3d46ea334c"), + internal: true, + compressed: true, + imported: false, + pubKey: hexToBytes("03f79bbde32af42dde98195f011d95982602fcd0dab657fe4a1f49f9d5ada1e02d"), + privKey: hexToBytes("bfef521317c65b018ae7e6d7ecc3aa700d5d0f7ea84d567be9270382d0b5e3e6"), + privKeyWIF: "L3eomUajnTDM3Pc8GU47qqXUFuCjvpqY7NYN9mH3x1ZFjDgiY4BU", + }, + { + address: "13NhXy2nCLMwNug1TZ6uwaWnxp3uTqdDQq", + addressHash: hexToBytes("1a0ad2a04fde3b2afe068057591e1871c289c4b8"), + internal: true, + compressed: true, + imported: false, + pubKey: hexToBytes("023ded84afe4fe91b52b45c3deb26fd263f749cbc27747dc964dae9e0739cbc579"), + privKey: hexToBytes("f506dffd4494c24006df7a35f3291f7ca0297a1a431557a1339bfed6f48738ca"), + privKeyWIF: "L5S1bVQUPqQb1Su82fLoSpnGCjcPfdAQE1pJxWRopJSBdYNDHESv", + }, + { + address: "1AY6yAHvojvpFcevAichLMnJfxgE8eSe4N", + addressHash: hexToBytes("689b0249c628265215fd1de6142d5d5594eb8dc2"), + internal: true, + compressed: true, + imported: false, + pubKey: hexToBytes("030f1e79f06824e10a259914ec310528bb2d5b8d6356341fe9dff55498591af6af"), + privKey: hexToBytes("b3629de8ef6a275b4ffae41aa2bbbc2952eb92282ea6402435abbb010ecc1fb8"), + privKeyWIF: "L3EQsGeEnyXmKaux54cG4DQeCSQDvGuvEuy3W2ss4geum7AtWaHw", + }, + { + address: "1Jc7An3JqjzRQULVr6Wh3iYR7miB6WPJCD", + addressHash: hexToBytes("c11dd8a3577978807a0453febedee2994a6144d4"), + internal: true, + compressed: true, + imported: false, + pubKey: hexToBytes("0317d7182e26b6ca3e0f3db531c474b9cab7a763a75eabff2e14ac92f62a793238"), + privKey: hexToBytes("ca747a7ef815ea0dbe68655272cecbfbd65f2a109019a9ed28e0d3dcaffe05c3"), + privKeyWIF: "L41Frac75RPbTELKzw1EGC2qCkdveiVumpmsyX4daAvyyCMxit1W", + }, + } + + // When the address manager is not in watching-only mode, unlocked it + // first to ensure that address generation works correctly when the + // address manager is unlocked and then locked later. These tests + // reverse the order done in the external tests which starts with a + // locked manager and unlock it afterwards. + if !tc.watchingOnly { + // Unlock the manager and retest all of the addresses to ensure the + // private information is valid as well. + if err := tc.manager.Unlock(privPassphrase); err != nil { + tc.t.Errorf("Unlock: unexpected error: %v", err) + return false + } + tc.unlocked = true + } + + prefix := testNamePrefix(tc) + " testInternalAddresses" + var addrs []waddrmgr.ManagedAddress + if tc.create { + prefix := prefix + " NextInternalAddress" + var err error + addrs, err = tc.manager.NextInternalAddresses(tc.account, 5) + if err != nil { + tc.t.Errorf("%s: unexpected error: %v", prefix, err) + return false + } + if len(addrs) != len(expectedAddrs) { + tc.t.Errorf("%s: unexpected number of addresses - got "+ + "%d, want %d", prefix, len(addrs), + len(expectedAddrs)) + return false + } + } + + // Setup a closure to test the results since the same tests need to be + // repeated with the manager locked and unlocked. + testResults := func() bool { + // Ensure the returned addresses are the expected ones. When + // not in the create phase, there will be no addresses in the + // addrs slice, so this really only runs during the first phase + // of the tests. + for i := 0; i < len(addrs); i++ { + prefix := fmt.Sprintf("%s InternalAddress #%d", prefix, i) + if !testAddress(tc, prefix, addrs[i], &expectedAddrs[i]) { + return false + } + } + + // Ensure the last internal address is the expected one. + liaPrefix := prefix + " LastInternalAddress" + lastAddr, err := tc.manager.LastInternalAddress(tc.account) + if err != nil { + tc.t.Errorf("%s: unexpected error: %v", liaPrefix, err) + return false + } + if !testAddress(tc, liaPrefix, lastAddr, &expectedAddrs[len(expectedAddrs)-1]) { + return false + } + + // Now, use the Address API to retrieve each of the expected new + // addresses and ensure they're accurate. + net := tc.manager.Net() + for i := 0; i < len(expectedAddrs); i++ { + pkHash := expectedAddrs[i].addressHash + utilAddr, err := btcutil.NewAddressPubKeyHash(pkHash, net) + if err != nil { + tc.t.Errorf("%s NewAddressPubKeyHash #%d: "+ + "unexpected error: %v", prefix, i, err) + return false + } + + prefix := fmt.Sprintf("%s Address #%d", prefix, i) + addr, err := tc.manager.Address(utilAddr) + if err != nil { + tc.t.Errorf("%s: unexpected error: %v", prefix, + err) + return false + } + + if !testAddress(tc, prefix, addr, &expectedAddrs[i]) { + return false + } + } + + return true + } + + // The address manager could either be locked or unlocked here depending + // on whether or not it's a watching-only manager. When it's unlocked, + // this will test both the public and private address data are accurate. + // When it's locked, it must be watching-only, so only the public + // address information is tested and the private functions are checked + // to ensure they return the expected ErrWatchingOnly error. + if !testResults() { + return false + } + + // Everything after this point involves locking the address manager and + // retesting the addresses with a locked manager. However, for + // watching-only mode, this has already happened, so just exit now in + // that case. + if tc.watchingOnly { + return true + } + + // Lock the manager and retest all of the addresses to ensure the + // public information remains valid and the private functions return + // the expected error. + if err := tc.manager.Lock(); err != nil { + tc.t.Errorf("Lock: unexpected error: %v", err) + return false + } + tc.unlocked = false + if !testResults() { + return false + } + + return true +} + +// testLocking tests the basic locking semantics of the address manager work +// as expected. Other tests ensure addresses behave as expected under locked +// and unlocked conditions. +func testLocking(tc *testContext) bool { + if tc.unlocked { + tc.t.Error("testLocking called with an unlocked manager") + return false + } + if !tc.manager.IsLocked() { + tc.t.Error("IsLocked: returned false on locked manager") + return false + } + + // Locking an already lock manager should return an error. The error + // should be ErrLocked or ErrWatchingOnly depending on the type of the + // address manager. + err := tc.manager.Lock() + wantErrCode := waddrmgr.ErrLocked + if tc.watchingOnly { + wantErrCode = waddrmgr.ErrWatchingOnly + } + if !checkManagerError(tc.t, "Lock", err, wantErrCode) { + return false + } + + // Ensure unlocking with the correct passphrase doesn't return any + // unexpected errors and the manager properly reports it is unlocked. + // Since watching-only address managers can't be unlocked, also ensure + // the correct error for that case. + err = tc.manager.Unlock(privPassphrase) + if tc.watchingOnly { + if !checkManagerError(tc.t, "Unlock", err, waddrmgr.ErrWatchingOnly) { + return false + } + } else if err != nil { + tc.t.Errorf("Unlock: unexpected error: %v", err) + return false + } + if !tc.watchingOnly && tc.manager.IsLocked() { + tc.t.Error("IsLocked: returned true on unlocked manager") + return false + } + + // Unlocking the manager again is allowed. Since watching-only address + // managers can't be unlocked, also ensure the correct error for that + // case. + err = tc.manager.Unlock(privPassphrase) + if tc.watchingOnly { + if !checkManagerError(tc.t, "Unlock2", err, waddrmgr.ErrWatchingOnly) { + return false + } + } else if err != nil { + tc.t.Errorf("Unlock: unexpected error: %v", err) + return false + } + if !tc.watchingOnly && tc.manager.IsLocked() { + tc.t.Error("IsLocked: returned true on unlocked manager") + return false + } + + // Unlocking the manager with an invalid passphrase must result in an + // error and a locked manager. + err = tc.manager.Unlock([]byte("invalidpassphrase")) + wantErrCode = waddrmgr.ErrWrongPassphrase + if tc.watchingOnly { + wantErrCode = waddrmgr.ErrWatchingOnly + } + if !checkManagerError(tc.t, "Unlock", err, wantErrCode) { + return false + } + if !tc.manager.IsLocked() { + tc.t.Error("IsLocked: manager is unlocked after failed unlock " + + "attempt") + return false + } + + return true +} + +// testImportPrivateKey tests that importing private keys works properly. It +// ensures they can be retrieved by Address after they have been imported and +// the addresses give the expected values when the manager is locked and +// unlocked. +// +// This function expects the manager is already locked when called and returns +// with the manager locked. +func testImportPrivateKey(tc *testContext) bool { + tests := []struct { + name string + in string + blockstamp waddrmgr.BlockStamp + expected expectedAddr + }{ + { + name: "wif for uncompressed pubkey address", + in: "5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ", + expected: expectedAddr{ + address: "1GAehh7TsJAHuUAeKZcXf5CnwuGuGgyX2S", + addressHash: hexToBytes("a65d1a239d4ec666643d350c7bb8fc44d2881128"), + internal: false, + imported: true, + compressed: false, + pubKey: hexToBytes("04d0de0aaeaefad02b8bdc8a01a1b8b11c696bd3" + + "d66a2c5f10780d95b7df42645cd85228a6fb29940e858e7e558" + + "42ae2bd115d1ed7cc0e82d934e929c97648cb0a"), + privKey: hexToBytes("0c28fca386c7a227600b2fe50b7cae11ec86d3bf1fbe471be89827e19d72aa1d"), + // privKeyWIF is set to the in field during tests + }, + }, + { + name: "wif for compressed pubkey address", + in: "KwdMAjGmerYanjeui5SHS7JkmpZvVipYvB2LJGU1ZxJwYvP98617", + expected: expectedAddr{ + address: "1LoVGDgRs9hTfTNJNuXKSpywcbdvwRXpmK", + addressHash: hexToBytes("d9351dcbad5b8f3b8bfa2f2cdc85c28118ca9326"), + internal: false, + imported: true, + compressed: true, + pubKey: hexToBytes("02d0de0aaeaefad02b8bdc8a01a1b8b11c696bd3d66a2c5f10780d95b7df42645c"), + privKey: hexToBytes("0c28fca386c7a227600b2fe50b7cae11ec86d3bf1fbe471be89827e19d72aa1d"), + // privKeyWIF is set to the in field during tests + }, + }, + } + + // The manager must be unlocked to import a private key, however a + // watching-only manager can't be unlocked. + if !tc.watchingOnly { + if err := tc.manager.Unlock(privPassphrase); err != nil { + tc.t.Errorf("Unlock: unexpected error: %v", err) + return false + } + tc.unlocked = true + } + + // Only import the private keys when in the create phase of testing. + tc.account = waddrmgr.ImportedAddrAccount + prefix := testNamePrefix(tc) + " testImportPrivateKey" + if tc.create { + for i, test := range tests { + test.expected.privKeyWIF = test.in + wif, err := btcutil.DecodeWIF(test.in) + if err != nil { + tc.t.Errorf("%s DecodeWIF #%d (%s): unexpected "+ + "error: %v", prefix, i, test.name, err) + continue + } + addr, err := tc.manager.ImportPrivateKey(wif, + &test.blockstamp) + if err != nil { + tc.t.Errorf("%s ImportPrivateKey #%d (%s): "+ + "unexpected error: %v", prefix, i, + test.name, err) + continue + } + if !testAddress(tc, prefix+" ImportPrivateKey", addr, + &test.expected) { + continue + } + } + } + + // Setup a closure to test the results since the same tests need to be + // repeated with the manager unlocked and locked. + net := tc.manager.Net() + testResults := func() bool { + failed := false + for i, test := range tests { + test.expected.privKeyWIF = test.in + + // Use the Address API to retrieve each of the expected + // new addresses and ensure they're accurate. + utilAddr, err := btcutil.NewAddressPubKeyHash( + test.expected.addressHash, net) + if err != nil { + tc.t.Errorf("%s NewAddressPubKeyHash #%d (%s): "+ + "unexpected error: %v", prefix, i, + test.name, err) + failed = true + continue + } + taPrefix := fmt.Sprintf("%s Address #%d (%s)", prefix, + i, test.name) + ma, err := tc.manager.Address(utilAddr) + if err != nil { + tc.t.Errorf("%s: unexpected error: %v", taPrefix, + err) + failed = true + continue + } + if !testAddress(tc, taPrefix, ma, &test.expected) { + failed = true + continue + } + } + + return !failed + } + + // The address manager could either be locked or unlocked here depending + // on whether or not it's a watching-only manager. When it's unlocked, + // this will test both the public and private address data are accurate. + // When it's locked, it must be watching-only, so only the public + // address information is tested and the private functions are checked + // to ensure they return the expected ErrWatchingOnly error. + if !testResults() { + return false + } + + // Everything after this point involves locking the address manager and + // retesting the addresses with a locked manager. However, for + // watching-only mode, this has already happened, so just exit now in + // that case. + if tc.watchingOnly { + return true + } + + // Lock the manager and retest all of the addresses to ensure the + // private information returns the expected error. + if err := tc.manager.Lock(); err != nil { + tc.t.Errorf("Lock: unexpected error: %v", err) + return false + } + tc.unlocked = false + if !testResults() { + return false + } + + return true +} + +// testImportScript tests that importing scripts works properly. It ensures +// they can be retrieved by Address after they have been imported and the +// addresses give the expected values when the manager is locked and unlocked. +// +// This function expects the manager is already locked when called and returns +// with the manager locked. +func testImportScript(tc *testContext) bool { + tests := []struct { + name string + in []byte + blockstamp waddrmgr.BlockStamp + expected expectedAddr + }{ + { + name: "p2sh uncompressed pubkey", + in: hexToBytes("41048b65a0e6bb200e6dac05e74281b1ab9a41e8" + + "0006d6b12d8521e09981da97dd96ac72d24d1a7d" + + "ed9493a9fc20fdb4a714808f0b680f1f1d935277" + + "48b5e3f629ffac"), + expected: expectedAddr{ + address: "3MbyWAu9UaoBewR3cArF1nwf4aQgVwzrA5", + addressHash: hexToBytes("da6e6a632d96dc5530d7b3c9f3017725d023093e"), + internal: false, + imported: true, + compressed: false, + // script is set to the in field during tests. + }, + }, + { + name: "p2sh multisig", + in: hexToBytes("524104cb9c3c222c5f7a7d3b9bd152f363a0b6d5" + + "4c9eb312c4d4f9af1e8551b6c421a6a4ab0e2910" + + "5f24de20ff463c1c91fcf3bf662cdde4783d4799" + + "f787cb7c08869b4104ccc588420deeebea22a7e9" + + "00cc8b68620d2212c374604e3487ca08f1ff3ae1" + + "2bdc639514d0ec8612a2d3c519f084d9a00cbbe3" + + "b53d071e9b09e71e610b036aa24104ab47ad1939" + + "edcb3db65f7fedea62bbf781c5410d3f22a7a3a5" + + "6ffefb2238af8627363bdf2ed97c1f89784a1aec" + + "db43384f11d2acc64443c7fc299cef0400421a53ae"), + expected: expectedAddr{ + address: "34CRZpt8j81rgh9QhzuBepqPi4cBQSjhjr", + addressHash: hexToBytes("1b800cec1fe92222f36a502c139bed47c5959715"), + internal: false, + imported: true, + compressed: false, + // script is set to the in field during tests. + }, + }, + } + + // The manager must be unlocked to import a private key and also for + // testing private data. However, a watching-only manager can't be + // unlocked. + if !tc.watchingOnly { + if err := tc.manager.Unlock(privPassphrase); err != nil { + tc.t.Errorf("Unlock: unexpected error: %v", err) + return false + } + tc.unlocked = true + } + + // Only import the scripts when in the create phase of testing. + tc.account = waddrmgr.ImportedAddrAccount + prefix := testNamePrefix(tc) + if tc.create { + for i, test := range tests { + test.expected.script = test.in + prefix := fmt.Sprintf("%s ImportScript #%d (%s)", prefix, + i, test.name) + + addr, err := tc.manager.ImportScript(test.in, + &test.blockstamp) + if err != nil { + tc.t.Errorf("%s: unexpected error: %v", prefix, + err) + continue + } + if !testAddress(tc, prefix, addr, &test.expected) { + continue + } + } + } + + // Setup a closure to test the results since the same tests need to be + // repeated with the manager unlocked and locked. + net := tc.manager.Net() + testResults := func() bool { + failed := false + for i, test := range tests { + test.expected.script = test.in + + // Use the Address API to retrieve each of the expected + // new addresses and ensure they're accurate. + utilAddr, err := btcutil.NewAddressScriptHash(test.in, net) + if err != nil { + tc.t.Errorf("%s NewAddressScriptHash #%d (%s): "+ + "unexpected error: %v", prefix, i, + test.name, err) + failed = true + continue + } + taPrefix := fmt.Sprintf("%s Address #%d (%s)", prefix, + i, test.name) + ma, err := tc.manager.Address(utilAddr) + if err != nil { + tc.t.Errorf("%s: unexpected error: %v", taPrefix, + err) + failed = true + continue + } + if !testAddress(tc, taPrefix, ma, &test.expected) { + failed = true + continue + } + } + + return !failed + } + + // The address manager could either be locked or unlocked here depending + // on whether or not it's a watching-only manager. When it's unlocked, + // this will test both the public and private address data are accurate. + // When it's locked, it must be watching-only, so only the public + // address information is tested and the private functions are checked + // to ensure they return the expected ErrWatchingOnly error. + if !testResults() { + return false + } + + // Everything after this point involves locking the address manager and + // retesting the addresses with a locked manager. However, for + // watching-only mode, this has already happened, so just exit now in + // that case. + if tc.watchingOnly { + return true + } + + // Lock the manager and retest all of the addresses to ensure the + // private information returns the expected error. + if err := tc.manager.Lock(); err != nil { + tc.t.Errorf("Lock: unexpected error: %v", err) + return false + } + tc.unlocked = false + if !testResults() { + return false + } + + return true +} + +// testChangePassphrase ensures changes both the public and privte passphrases +// works as intended. +func testChangePassphrase(tc *testContext) bool { + // Force an error when changing the passphrase due to failure to + // generate a new secret key by replacing the generation function one + // that intentionally errors. + testName := "ChangePassphrase (public) with invalid new secret key" + waddrmgr.TstReplaceNewSecretKeyFunc() + err := tc.manager.ChangePassphrase(pubPassphrase, pubPassphrase2, false) + if !checkManagerError(tc.t, testName, err, waddrmgr.ErrCrypto) { + return false + } + waddrmgr.TstResetNewSecretKeyFunc() + + // Attempt to change public passphrase with invalid old passphrase. + testName = "ChangePassphrase (public) with invalid old passphrase" + err = tc.manager.ChangePassphrase([]byte("bogus"), pubPassphrase2, false) + if !checkManagerError(tc.t, testName, err, waddrmgr.ErrWrongPassphrase) { + return false + } + + // Change the public passphrase. + testName = "ChangePassphrase (public)" + err = tc.manager.ChangePassphrase(pubPassphrase, pubPassphrase2, false) + if err != nil { + tc.t.Errorf("%s: unexpected error: %v", testName, err) + return false + } + + // Ensure the public passphrase was successfully changed. + if !tc.manager.TstCheckPublicPassphrase(pubPassphrase2) { + tc.t.Errorf("%s: passphrase does not match", testName) + return false + } + + // Change the private passphrase back to what it was. + err = tc.manager.ChangePassphrase(pubPassphrase2, pubPassphrase, false) + if err != nil { + tc.t.Errorf("%s: unexpected error: %v", testName, err) + return false + } + + // Attempt to change private passphrase with invalid old passphrase. + // The error should be ErrWrongPassphrase or ErrWatchingOnly depending + // on the type of the address manager. + testName = "ChangePassphrase (private) with invalid old passphrase" + err = tc.manager.ChangePassphrase([]byte("bogus"), privPassphrase2, true) + wantErrCode := waddrmgr.ErrWrongPassphrase + if tc.watchingOnly { + wantErrCode = waddrmgr.ErrWatchingOnly + } + if !checkManagerError(tc.t, testName, err, wantErrCode) { + return false + } + + // Everything after this point involves testing that the private + // passphrase for the address manager can be changed successfully. + // This is not possible for watching-only mode, so just exit now in that + // case. + if tc.watchingOnly { + return true + } + + // Change the private passphrase. + testName = "ChangePassphrase (private)" + err = tc.manager.ChangePassphrase(privPassphrase, privPassphrase2, true) + if err != nil { + tc.t.Errorf("%s: unexpected error: %v", testName, err) + return false + } + + // Unlock the manager with the new passphrase to ensure it changed as + // expected. + if err := tc.manager.Unlock(privPassphrase2); err != nil { + tc.t.Errorf("%s: failed to unlock with new private "+ + "passphrase: %v", testName, err) + return false + } + tc.unlocked = true + + // Change the private passphrase back to what it was while the manager + // is unlocked to ensure that path works properly as well. + err = tc.manager.ChangePassphrase(privPassphrase2, privPassphrase, true) + if err != nil { + tc.t.Errorf("%s: unexpected error: %v", testName, err) + return false + } + if tc.manager.IsLocked() { + tc.t.Errorf("%s: manager is locked", testName) + return false + } + + // Relock the manager for future tests. + if err := tc.manager.Lock(); err != nil { + tc.t.Errorf("Lock: unexpected error: %v", err) + return false + } + tc.unlocked = false + + return true +} + +// testManagerAPI tests the functions provided by the Manager API as well as +// the ManagedAddress, ManagedPubKeyAddress, and ManagedScriptAddress +// interfaces. +func testManagerAPI(tc *testContext) { + testLocking(tc) + testExternalAddresses(tc) + testInternalAddresses(tc) + testImportPrivateKey(tc) + testImportScript(tc) + testChangePassphrase(tc) +} + +// testExportWatchingOnly tests various facets of a watching-only address +// manager such as running the full set of API tests against a newly exported +// copy as well as when it is opened. +func testExportWatchingOnly(tc *testContext) bool { + // Export the manager as watching-only. + woMgrName := "mgrtestwo.bin" + _ = os.Remove(woMgrName) + mgr, err := tc.manager.ExportWatchingOnly(woMgrName, pubPassphrase) + if err != nil { + tc.t.Errorf("ExportWatchingOnly: unexpected error: %v", err) + return false + } + defer os.Remove(woMgrName) + // NOTE: Not using deferred close here since part of the tests is + // explicitly closing the manager and then opening the existing one. + + // Exporting to an existing manager should fail. + _, err = tc.manager.ExportWatchingOnly(woMgrName, pubPassphrase) + if !checkManagerError(tc.t, "Export watching-only", err, waddrmgr.ErrAlreadyExists) { + mgr.Close() + return false + } + + // Run all of the manager API tests and close the manager. + testManagerAPI(&testContext{ + t: tc.t, + manager: mgr, + account: 0, + create: false, + watchingOnly: true, + }) + mgr.Close() + + // Open the watching-only manager and run all the tests again. + mgr, err = waddrmgr.Open(woMgrName, pubPassphrase, &btcnet.MainNetParams) + if err != nil { + tc.t.Errorf("Open Watching-Only: unexpected error: %v", err) + return false + } + defer mgr.Close() + + testManagerAPI(&testContext{ + t: tc.t, + manager: mgr, + account: 0, + create: false, + watchingOnly: true, + }) + + return true +} + +// testSync tests various facets of setting the manager sync state. +func testSync(tc *testContext) bool { + tests := []struct { + name string + hash *btcwire.ShaHash + }{ + { + name: "Block 1", + hash: newShaHash("00000000839a8e6886ab5951d76f411475428afc90947ee320161bbf18eb6048"), + }, + { + name: "Block 2", + hash: newShaHash("000000006a625f06636b8bb6ac7b960a8d03705d1ace08b1a19da3fdcc99ddbd"), + }, + { + name: "Block 3", + hash: newShaHash("0000000082b5015589a3fdf2d4baff403e6f0be035a5d9742c1cae6295464449"), + }, + { + name: "Block 4", + hash: newShaHash("000000004ebadb55ee9096c9a2f8880e09da59c0d68b1c228da88e48844a1485"), + }, + { + name: "Block 5", + hash: newShaHash("000000009b7262315dbf071787ad3656097b892abffd1f95a1a022f896f533fc"), + }, + { + name: "Block 6", + hash: newShaHash("000000003031a0e73735690c5a1ff2a4be82553b2a12b776fbd3a215dc8f778d"), + }, + { + name: "Block 7", + hash: newShaHash("0000000071966c2b1d065fd446b1e485b2c9d9594acd2007ccbd5441cfc89444"), + }, + { + name: "Block 8", + hash: newShaHash("00000000408c48f847aa786c2268fc3e6ec2af68e8468a34a28c61b7f1de0dc6"), + }, + { + name: "Block 9", + hash: newShaHash("000000008d9dc510f23c2657fc4f67bea30078cc05a90eb89e84cc475c080805"), + }, + { + name: "Block 10", + hash: newShaHash("000000002c05cc2e78923c34df87fd108b22221ac6076c18f3ade378a4d915e9"), + }, + { + name: "Block 11", + hash: newShaHash("0000000097be56d606cdd9c54b04d4747e957d3608abe69198c661f2add73073"), + }, + { + name: "Block 12", + hash: newShaHash("0000000027c2488e2510d1acf4369787784fa20ee084c258b58d9fbd43802b5e"), + }, + { + name: "Block 13", + hash: newShaHash("000000005c51de2031a895adc145ee2242e919a01c6d61fb222a54a54b4d3089"), + }, + { + name: "Block 14", + hash: newShaHash("0000000080f17a0c5a67f663a9bc9969eb37e81666d9321125f0e293656f8a37"), + }, + { + name: "Block 15", + hash: newShaHash("00000000b3322c8c3ef7d2cf6da009a776e6a99ee65ec5a32f3f345712238473"), + }, + { + name: "Block 16", + hash: newShaHash("00000000174a25bb399b009cc8deff1c4b3ea84df7e93affaaf60dc3416cc4f5"), + }, + { + name: "Block 17", + hash: newShaHash("000000003ff1d0d70147acfbef5d6a87460ff5bcfce807c2d5b6f0a66bfdf809"), + }, + { + name: "Block 18", + hash: newShaHash("000000008693e98cf893e4c85a446b410bb4dfa129bd1be582c09ed3f0261116"), + }, + { + name: "Block 19", + hash: newShaHash("00000000841cb802ca97cf20fb9470480cae9e5daa5d06b4a18ae2d5dd7f186f"), + }, + { + name: "Block 20", + hash: newShaHash("0000000067a97a2a37b8f190a17f0221e9c3f4fa824ddffdc2e205eae834c8d7"), + }, + { + name: "Block 21", + hash: newShaHash("000000006f016342d1275be946166cff975c8b27542de70a7113ac6d1ef3294f"), + }, + } + + // Ensure there are enough test vectors to prove the maximum number of + // recent hashes is working properly. + maxRecentHashes := waddrmgr.TstMaxRecentHashes + if len(tests) < maxRecentHashes-1 { + tc.t.Errorf("Not enough hashes to test max recent hashes - "+ + "need %d, have %d", maxRecentHashes-1, len(tests)) + return false + } + + for i, test := range tests { + blockStamp := waddrmgr.BlockStamp{ + Height: int32(i) + 1, + Hash: *test.hash, + } + if err := tc.manager.SetSyncedTo(&blockStamp); err != nil { + tc.t.Errorf("SetSyncedTo unexpected err: %v", err) + return false + } + + // Ensure the manager now claims it is synced to the block stamp + // that was just set. + gotBlockStamp := tc.manager.SyncedTo() + if gotBlockStamp != blockStamp { + tc.t.Errorf("SyncedTo unexpected block stamp -- got "+ + "%v, want %v", gotBlockStamp, blockStamp) + return false + } + + // Ensure the recent blocks iterator works properly. + j := 0 + iter := tc.manager.NewIterateRecentBlocks() + for cont := iter != nil; cont; cont = iter.Prev() { + wantHeight := int32(i) - int32(j) + 1 + var wantHash *btcwire.ShaHash + if wantHeight == 0 { + wantHash = btcnet.MainNetParams.GenesisHash + } else { + wantHash = tests[wantHeight-1].hash + } + + gotBS := iter.BlockStamp() + if gotBS.Height != wantHeight { + tc.t.Errorf("NewIterateRecentBlocks block "+ + "stamp height mismatch -- got %d, "+ + "want %d", gotBS.Height, wantHeight) + return false + } + if gotBS.Hash != *wantHash { + tc.t.Errorf("NewIterateRecentBlocks block "+ + "stamp hash mismatch -- got %v, "+ + "want %v", gotBS.Hash, wantHash) + return false + } + j++ + } + + // Ensure the maximum number of recent hashes works as expected. + if i >= maxRecentHashes-1 && j != maxRecentHashes { + tc.t.Errorf("NewIterateRecentBlocks iterated more than "+ + "the max number of expected blocks -- got %d, "+ + "want %d", j, maxRecentHashes) + return false + } + } + + // Ensure rollback to block in recent history works as expected. + blockStamp := waddrmgr.BlockStamp{ + Height: 10, + Hash: *tests[9].hash, + } + if err := tc.manager.SetSyncedTo(&blockStamp); err != nil { + tc.t.Errorf("SetSyncedTo unexpected err on rollback to block "+ + "in recent history: %v", err) + return false + } + gotBlockStamp := tc.manager.SyncedTo() + if gotBlockStamp != blockStamp { + tc.t.Errorf("SyncedTo unexpected block stamp on rollback -- "+ + "got %v, want %v", gotBlockStamp, blockStamp) + return false + } + + // Ensure syncing to a block that is in the future as compared to the + // current block stamp clears the old recent blocks. + blockStamp = waddrmgr.BlockStamp{ + Height: 100, + Hash: *newShaHash("000000007bc154e0fa7ea32218a72fe2c1bb9f86cf8c9ebf9a715ed27fdb229a"), + } + if err := tc.manager.SetSyncedTo(&blockStamp); err != nil { + tc.t.Errorf("SetSyncedTo unexpected err on future block stamp: "+ + "%v", err) + return false + } + numRecentBlocks := 0 + iter := tc.manager.NewIterateRecentBlocks() + for cont := iter != nil; cont; cont = iter.Prev() { + numRecentBlocks++ + } + if numRecentBlocks != 1 { + tc.t.Errorf("Unexpected number of blocks after future block "+ + "stamp -- got %d, want %d", numRecentBlocks, 1) + return false + } + + // Rollback to a block that is not in the recent block history and + // ensure it results in only that block. + blockStamp = waddrmgr.BlockStamp{ + Height: 1, + Hash: *tests[0].hash, + } + if err := tc.manager.SetSyncedTo(&blockStamp); err != nil { + tc.t.Errorf("SetSyncedTo unexpected err on rollback to block "+ + "not in recent history: %v", err) + return false + } + gotBlockStamp = tc.manager.SyncedTo() + if gotBlockStamp != blockStamp { + tc.t.Errorf("SyncedTo unexpected block stamp on rollback to "+ + "block not in recent history -- got %v, want %v", + gotBlockStamp, blockStamp) + return false + } + numRecentBlocks = 0 + iter = tc.manager.NewIterateRecentBlocks() + for cont := iter != nil; cont; cont = iter.Prev() { + numRecentBlocks++ + } + if numRecentBlocks != 1 { + tc.t.Errorf("Unexpected number of blocks after rollback to "+ + "block not in recent history -- got %d, want %d", + numRecentBlocks, 1) + return false + } + + // Ensure syncing the manager to nil results in the synced to state + // being the earliest block (genesis block in this case). + if err := tc.manager.SetSyncedTo(nil); err != nil { + tc.t.Errorf("SetSyncedTo unexpected err on nil: %v", err) + return false + } + blockStamp = waddrmgr.BlockStamp{ + Height: 0, + Hash: *btcnet.MainNetParams.GenesisHash, + } + gotBlockStamp = tc.manager.SyncedTo() + if gotBlockStamp != blockStamp { + tc.t.Errorf("SyncedTo unexpected block stamp on nil -- "+ + "got %v, want %v", gotBlockStamp, blockStamp) + return false + } + + return true +} + +// TestManager performs a full suite of tests against the address manager API. +// It makes use of a test context because the address manager is persistent and +// much of the testing involves having specific state. +func TestManager(t *testing.T) { + // Open manager that does not exist to ensure the expected error is + // returned. + mgrName := "mgrtest.bin" + _ = os.Remove(mgrName) + _, err := waddrmgr.Open(mgrName, pubPassphrase, &btcnet.MainNetParams) + if !checkManagerError(t, "Open non-existant", err, waddrmgr.ErrNoExist) { + return + } + + // Create a new manager. + mgr, err := waddrmgr.Create(mgrName, seed, pubPassphrase, privPassphrase, + &btcnet.MainNetParams) + if err != nil { + t.Errorf("Create: unexpected error: %v", err) + return + } + defer os.Remove(mgrName) + // NOTE: Not using deferred close here since part of the tests is + // explicitly closing the manager and then opening the existing one. + + // Attempt to create the manager again to ensure the expected error is + // returned. + _, err = waddrmgr.Create(mgrName, seed, pubPassphrase, privPassphrase, + &btcnet.MainNetParams) + if !checkManagerError(t, "Create existing", err, waddrmgr.ErrAlreadyExists) { + mgr.Close() + return + } + + // Run all of the manager API tests in create mode and close the + // manager after they've completed + testManagerAPI(&testContext{ + t: t, + manager: mgr, + account: 0, + create: true, + watchingOnly: false, + }) + mgr.Close() + + // Open the manager and run all the tests again in open mode which + // avoids reinserting new addresses like the create mode tests do. + mgr, err = waddrmgr.Open(mgrName, pubPassphrase, &btcnet.MainNetParams) + if err != nil { + t.Errorf("Open: unexpected error: %v", err) + return + } + defer mgr.Close() + + tc := &testContext{ + t: t, + manager: mgr, + account: 0, + create: false, + watchingOnly: false, + } + testManagerAPI(tc) + + // Now that the address manager has been tested in both the newly + // created and opened modes, test a watching-only version. + testExportWatchingOnly(tc) + + // Ensure that the manager sync state functionality works as expected. + testSync(tc) + + // Unlock the manager so it can be closed with it unlocked to ensure + // it works without issue. + if err := mgr.Unlock(privPassphrase); err != nil { + t.Errorf("Unlock: unexpected error: %v", err) + } +} diff --git a/waddrmgr/sync.go b/waddrmgr/sync.go new file mode 100644 index 0000000..0f1234b --- /dev/null +++ b/waddrmgr/sync.go @@ -0,0 +1,241 @@ +/* + * Copyright (c) 2014 Conformal Systems LLC + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package waddrmgr + +import ( + "sync" + + "github.com/conformal/btcwire" +) + +const ( + // maxRecentHashes is the maximum number of hashes to keep in history + // for the purposes of rollbacks. + maxRecentHashes = 20 +) + +// BlockStamp defines a block (by height and a unique hash) and is +// used to mark a point in the blockchain that an address manager element is +// synced to. +type BlockStamp struct { + Height int32 + Hash btcwire.ShaHash +} + +// syncState houses the sync state of the manager. It consists of the recently +// seen blocks as height, as well as the start and current sync block stamps. +type syncState struct { + // startBlock is the first block that can be safely used to start a + // rescan. It is either the block the manager was created with, or + // the earliest block provided with imported addresses or scripts. + startBlock BlockStamp + + // syncedTo is the current block the addresses in the manager are known + // to be synced against. + syncedTo BlockStamp + + // recentHeight is the most recently seen sync height. + recentHeight int32 + + // recentHashes is a list of the last several seen block hashes. + recentHashes []btcwire.ShaHash +} + +// iter returns a BlockIterator that can be used to iterate over the recently +// seen blocks in the sync state. +func (s *syncState) iter(mtx *sync.RWMutex) *BlockIterator { + if s.recentHeight == -1 || len(s.recentHashes) == 0 { + return nil + } + return &BlockIterator{ + mtx: mtx, + height: s.recentHeight, + index: len(s.recentHashes) - 1, + syncInfo: s, + } +} + +// newSyncState returns a new sync state with the provided parameters. +func newSyncState(startBlock, syncedTo *BlockStamp, recentHeight int32, + recentHashes []btcwire.ShaHash) *syncState { + + return &syncState{ + startBlock: *startBlock, + syncedTo: *syncedTo, + recentHeight: recentHeight, + recentHashes: recentHashes, + } +} + +// BlockIterator allows for the forwards and backwards iteration of recently +// seen blocks. +type BlockIterator struct { + mtx *sync.RWMutex + height int32 + index int + syncInfo *syncState +} + +// Next returns the next recently seen block or false if there is not one. +func (it *BlockIterator) Next() bool { + it.mtx.RLock() + defer it.mtx.RUnlock() + + if it.index+1 >= len(it.syncInfo.recentHashes) { + return false + } + it.index++ + return true +} + +// Prev returns the previous recently seen block or false if there is not one. +func (it *BlockIterator) Prev() bool { + it.mtx.RLock() + defer it.mtx.RUnlock() + + if it.index-1 < 0 { + return false + } + it.index-- + return true +} + +// BlockStamp returns the block stamp associated with the recently seen block +// the iterator is currently pointing to. +func (it *BlockIterator) BlockStamp() BlockStamp { + it.mtx.RLock() + defer it.mtx.RUnlock() + + return BlockStamp{ + Height: it.syncInfo.recentHeight - + int32(len(it.syncInfo.recentHashes)-1-it.index), + Hash: it.syncInfo.recentHashes[it.index], + } +} + +// NewIterateRecentBlocks returns an iterator for recently-seen blocks. +// The iterator starts at the most recently-added block, and Prev should +// be used to access earlier blocks. +// +// NOTE: Ideally this should not really be a part of the address manager as it +// is intended for syncing purposes. It is being exposed here for now to go +// with the other syncing code. Ultimately, all syncing code should probably +// go into its own package and share the data store. +func (m *Manager) NewIterateRecentBlocks() *BlockIterator { + m.mtx.RLock() + defer m.mtx.RUnlock() + + return m.syncState.iter(&m.mtx) +} + +// SetSyncedTo marks the address manager to be in sync with the recently-seen +// block described by the blockstamp. When the provided blockstamp is nil, +// the oldest blockstamp of the block the manager was created at and of all +// imported addresses will be used. This effectively allows the manager to be +// marked as unsynced back to the oldest known point any of the addresses have +// appeared in the block chain. +func (m *Manager) SetSyncedTo(bs *BlockStamp) error { + m.mtx.Lock() + defer m.mtx.Unlock() + + // Update the recent history. + // + // NOTE: The values in the memory sync state aren't directly modified + // here in case the forthcoming db update fails. The memory sync state + // is updated with these values as needed after the db updates. + recentHeight := m.syncState.recentHeight + recentHashes := m.syncState.recentHashes + if bs == nil { + // Use the stored start blockstamp and reset recent hashes and + // height when the provided blockstamp is nil. + bs = &m.syncState.startBlock + recentHeight = m.syncState.startBlock.Height + recentHashes = nil + + } else if bs.Height < recentHeight { + // When the new block stamp height is prior to the most recently + // seen height, a rollback is being performed. Thus, when the + // previous block stamp is already saved, remove anything after + // it. Otherwise, the rollback must be too far in history, so + // clear the recent hashes and set the recent height to the + // current block stamp height. + numHashes := len(recentHashes) + idx := numHashes - 1 - int(recentHeight-bs.Height) + if idx >= 0 && idx < numHashes && recentHashes[idx] == bs.Hash { + // subslice out the removed hashes. + recentHeight = bs.Height + recentHashes = recentHashes[:idx] + } else { + recentHeight = bs.Height + recentHashes = nil + } + + } else if bs.Height != recentHeight+1 { + // At this point the new block stamp height is after the most + // recently seen block stamp, so it should be the next height in + // sequence. When this is not the case, the recent history is + // no longer valid, so clear the recent hashes and set the + // recent height to the current block stamp height. + recentHeight = bs.Height + recentHashes = nil + } else { + // The only case left is when the new block stamp height is the + // next height in sequence after the most recently seen block + // stamp, so update it accordingly. + recentHeight = bs.Height + } + + // Enforce maximum number of recent hashes. + if len(recentHashes) == maxRecentHashes { + // Shift everything down one position and add the new hash in + // the last position. + copy(recentHashes, recentHashes[1:]) + recentHashes[maxRecentHashes-1] = bs.Hash + } else { + recentHashes = append(recentHashes, bs.Hash) + } + + // Update the database. + err := m.db.Update(func(tx *managerTx) error { + err := tx.PutSyncedTo(bs) + if err != nil { + return err + } + + return tx.PutRecentBlocks(recentHeight, recentHashes) + }) + if err != nil { + return err + } + + // Update memory now that the database is updated. + m.syncState.syncedTo = *bs + m.syncState.recentHashes = recentHashes + m.syncState.recentHeight = recentHeight + return nil +} + +// SyncedTo returns details about the block height and hash that the address +// manager is synced through at the very least. The intention is that callers +// can use this information for intelligently initiating rescans to sync back to +// the best chain from the last known good block. +func (m *Manager) SyncedTo() BlockStamp { + m.mtx.Lock() + defer m.mtx.Unlock() + + return m.syncState.syncedTo +} diff --git a/waddrmgr/test_coverage.txt b/waddrmgr/test_coverage.txt new file mode 100644 index 0000000..3b08655 --- /dev/null +++ b/waddrmgr/test_coverage.txt @@ -0,0 +1,126 @@ + +github.com/conformal/btcwallet/waddrmgr/db.go serializeBIP0044AccountRow 100.00% (19/19) +github.com/conformal/btcwallet/waddrmgr/manager.go Manager.lock 100.00% (12/12) +github.com/conformal/btcwallet/waddrmgr/db.go serializeScriptAddress 100.00% (10/10) +github.com/conformal/btcwallet/waddrmgr/db.go serializeImportedAddress 100.00% (10/10) +github.com/conformal/btcwallet/waddrmgr/db.go serializeAddressRow 100.00% (9/9) +github.com/conformal/btcwallet/waddrmgr/manager.go Manager.Address 100.00% (8/8) +github.com/conformal/btcwallet/waddrmgr/manager.go Manager.Lock 100.00% (8/8) +github.com/conformal/btcwallet/waddrmgr/address.go scriptAddress.Script 100.00% (7/7) +github.com/conformal/btcwallet/waddrmgr/db.go serializeAccountRow 100.00% (6/6) +github.com/conformal/btcwallet/waddrmgr/sync.go BlockIterator.Prev 100.00% (6/6) +github.com/conformal/btcwallet/waddrmgr/address.go zeroBigInt 100.00% (5/5) +github.com/conformal/btcwallet/waddrmgr/manager.go Manager.zeroSensitivePublicData 100.00% (5/5) +github.com/conformal/btcwallet/waddrmgr/address.go managedAddress.lock 100.00% (4/4) +github.com/conformal/btcwallet/waddrmgr/address.go scriptAddress.lock 100.00% (4/4) +github.com/conformal/btcwallet/waddrmgr/db.go serializeChainedAddress 100.00% (4/4) +github.com/conformal/btcwallet/waddrmgr/address.go managedAddress.ExportPrivKey 100.00% (4/4) +github.com/conformal/btcwallet/waddrmgr/manager.go fileExists 100.00% (4/4) +github.com/conformal/btcwallet/waddrmgr/error.go ManagerError.Error 100.00% (3/3) +github.com/conformal/btcwallet/waddrmgr/error.go ErrorCode.String 100.00% (3/3) +github.com/conformal/btcwallet/waddrmgr/address.go managedAddress.pubKeyBytes 100.00% (3/3) +github.com/conformal/btcwallet/waddrmgr/sync.go Manager.NewIterateRecentBlocks 100.00% (3/3) +github.com/conformal/btcwallet/waddrmgr/sync.go BlockIterator.BlockStamp 100.00% (3/3) +github.com/conformal/btcwallet/waddrmgr/db.go accountKey 100.00% (3/3) +github.com/conformal/btcwallet/waddrmgr/sync.go Manager.SyncedTo 100.00% (3/3) +github.com/conformal/btcwallet/waddrmgr/db.go managerTx.PutAccountInfo 100.00% (3/3) +github.com/conformal/btcwallet/waddrmgr/manager.go Manager.IsLocked 100.00% (3/3) +github.com/conformal/btcwallet/waddrmgr/db.go managerTx.PutImportedAddress 100.00% (3/3) +github.com/conformal/btcwallet/waddrmgr/db.go managerTx.ExistsAddress 100.00% (3/3) +github.com/conformal/btcwallet/waddrmgr/address.go zero 100.00% (2/2) +github.com/conformal/btcwallet/waddrmgr/address.go scriptAddress.Account 100.00% (1/1) +github.com/conformal/btcwallet/waddrmgr/address.go scriptAddress.Compressed 100.00% (1/1) +github.com/conformal/btcwallet/waddrmgr/sync.go newSyncState 100.00% (1/1) +github.com/conformal/btcwallet/waddrmgr/address.go scriptAddress.Internal 100.00% (1/1) +github.com/conformal/btcwallet/waddrmgr/manager.go cryptoKey.CopyBytes 100.00% (1/1) +github.com/conformal/btcwallet/waddrmgr/manager.go newManager 100.00% (1/1) +github.com/conformal/btcwallet/waddrmgr/address.go managedAddress.AddrHash 100.00% (1/1) +github.com/conformal/btcwallet/waddrmgr/manager.go defaultNewSecretKey 100.00% (1/1) +github.com/conformal/btcwallet/waddrmgr/address.go scriptAddress.Imported 100.00% (1/1) +github.com/conformal/btcwallet/waddrmgr/address.go scriptAddress.AddrHash 100.00% (1/1) +github.com/conformal/btcwallet/waddrmgr/address.go scriptAddress.Address 100.00% (1/1) +github.com/conformal/btcwallet/waddrmgr/address.go managedAddress.Account 100.00% (1/1) +github.com/conformal/btcwallet/waddrmgr/address.go managedAddress.Internal 100.00% (1/1) +github.com/conformal/btcwallet/waddrmgr/manager.go Manager.Net 100.00% (1/1) +github.com/conformal/btcwallet/waddrmgr/address.go managedAddress.ExportPubKey 100.00% (1/1) +github.com/conformal/btcwallet/waddrmgr/error.go managerError 100.00% (1/1) +github.com/conformal/btcwallet/waddrmgr/address.go managedAddress.PubKey 100.00% (1/1) +github.com/conformal/btcwallet/waddrmgr/manager.go cryptoKey.Bytes 100.00% (1/1) +github.com/conformal/btcwallet/waddrmgr/address.go managedAddress.Compressed 100.00% (1/1) +github.com/conformal/btcwallet/waddrmgr/address.go managedAddress.Address 100.00% (1/1) +github.com/conformal/btcwallet/waddrmgr/address.go managedAddress.Imported 100.00% (1/1) +github.com/conformal/btcwallet/waddrmgr/sync.go Manager.SetSyncedTo 93.94% (31/33) +github.com/conformal/btcwallet/waddrmgr/address.go managedAddress.PrivKey 91.67% (11/12) +github.com/conformal/btcwallet/waddrmgr/db.go deserializeBIP0044AccountRow 90.48% (19/21) +github.com/conformal/btcwallet/waddrmgr/manager.go Manager.keyToManaged 90.00% (9/10) +github.com/conformal/btcwallet/waddrmgr/db.go managerTx.FetchCryptoKeys 88.89% (16/18) +github.com/conformal/btcwallet/waddrmgr/manager.go Manager.Close 88.89% (8/9) +github.com/conformal/btcwallet/waddrmgr/address.go newManagedAddressWithoutPrivKey 87.50% (7/8) +github.com/conformal/btcwallet/waddrmgr/db.go managerTx.PutRecentBlocks 85.71% (12/14) +github.com/conformal/btcwallet/waddrmgr/manager.go Open 85.71% (6/7) +github.com/conformal/btcwallet/waddrmgr/db.go deserializeScriptAddress 84.62% (11/13) +github.com/conformal/btcwallet/waddrmgr/db.go deserializeImportedAddress 84.62% (11/13) +github.com/conformal/btcwallet/waddrmgr/db.go managerTx.FetchRecentBlocks 84.62% (11/13) +github.com/conformal/btcwallet/waddrmgr/db.go managerTx.FetchMasterKeyParams 84.62% (11/13) +github.com/conformal/btcwallet/waddrmgr/db.go deserializeAddressRow 83.33% (10/12) +github.com/conformal/btcwallet/waddrmgr/manager.go Manager.loadAndCacheAddress 83.33% (10/12) +github.com/conformal/btcwallet/waddrmgr/address.go managedAddress.unlock 81.82% (9/11) +github.com/conformal/btcwallet/waddrmgr/address.go scriptAddress.unlock 81.82% (9/11) +github.com/conformal/btcwallet/waddrmgr/manager.go Manager.nextAddresses 80.00% (52/65) +github.com/conformal/btcwallet/waddrmgr/db.go managerTx.PutScriptAddress 80.00% (4/5) +github.com/conformal/btcwallet/waddrmgr/manager.go Manager.ChangePassphrase 79.10% (53/67) +github.com/conformal/btcwallet/waddrmgr/db.go managerTx.PutChainedAddress 78.26% (18/23) +github.com/conformal/btcwallet/waddrmgr/db.go managerTx.FetchSyncedTo 77.78% (7/9) +github.com/conformal/btcwallet/waddrmgr/db.go managerTx.PutStartBlock 77.78% (7/9) +github.com/conformal/btcwallet/waddrmgr/db.go managerTx.FetchStartBlock 77.78% (7/9) +github.com/conformal/btcwallet/waddrmgr/db.go managerTx.PutSyncedTo 77.78% (7/9) +github.com/conformal/btcwallet/waddrmgr/manager.go Manager.existsAddress 77.78% (7/9) +github.com/conformal/btcwallet/waddrmgr/db.go deserializeAccountRow 77.78% (7/9) +github.com/conformal/btcwallet/waddrmgr/manager.go Manager.ExportWatchingOnly 75.00% (12/16) +github.com/conformal/btcwallet/waddrmgr/address.go newManagedAddressFromExtKey 75.00% (12/16) +github.com/conformal/btcwallet/waddrmgr/address.go newManagedAddress 75.00% (9/12) +github.com/conformal/btcwallet/waddrmgr/db.go managerTx.PutWatchingOnly 75.00% (6/8) +github.com/conformal/btcwallet/waddrmgr/db.go managerTx.PutNumAccounts 75.00% (6/8) +github.com/conformal/btcwallet/waddrmgr/address.go newScriptAddress 75.00% (3/4) +github.com/conformal/btcwallet/waddrmgr/manager.go defaultNewCryptoKey 75.00% (3/4) +github.com/conformal/btcwallet/waddrmgr/manager.go Manager.chainAddressRowToManaged 75.00% (3/4) +github.com/conformal/btcwallet/waddrmgr/manager.go checkBranchKeys 75.00% (3/4) +github.com/conformal/btcwallet/waddrmgr/manager.go Manager.deriveKeyFromPath 75.00% (3/4) +github.com/conformal/btcwallet/waddrmgr/manager.go loadManager 72.55% (37/51) +github.com/conformal/btcwallet/waddrmgr/db.go managerTx.putAddress 71.43% (5/7) +github.com/conformal/btcwallet/waddrmgr/db.go deserializeChainedAddress 71.43% (5/7) +github.com/conformal/btcwallet/waddrmgr/manager.go Manager.deriveKey 69.23% (9/13) +github.com/conformal/btcwallet/waddrmgr/manager.go Manager.ImportScript 67.44% (29/43) +github.com/conformal/btcwallet/waddrmgr/manager.go Manager.Unlock 67.35% (33/49) +github.com/conformal/btcwallet/waddrmgr/db.go managerTx.FetchAddress 66.67% (10/15) +github.com/conformal/btcwallet/waddrmgr/manager.go Manager.importedAddressRowToManaged 66.67% (10/15) +github.com/conformal/btcwallet/waddrmgr/db.go managerTx.PutMasterKeyParams 66.67% (8/12) +github.com/conformal/btcwallet/waddrmgr/manager.go Manager.LastInternalAddress 66.67% (6/9) +github.com/conformal/btcwallet/waddrmgr/manager.go Manager.LastExternalAddress 66.67% (6/9) +github.com/conformal/btcwallet/waddrmgr/db.go managerTx.putAccountRow 66.67% (4/6) +github.com/conformal/btcwallet/waddrmgr/manager.go Manager.rowInterfaceToManaged 66.67% (4/6) +github.com/conformal/btcwallet/waddrmgr/manager.go Manager.NextExternalAddresses 66.67% (4/6) +github.com/conformal/btcwallet/waddrmgr/manager.go Manager.NextInternalAddresses 66.67% (4/6) +github.com/conformal/btcwallet/waddrmgr/db.go managerTx.FetchWatchingOnly 66.67% (4/6) +github.com/conformal/btcwallet/waddrmgr/sync.go syncState.iter 66.67% (2/3) +github.com/conformal/btcwallet/waddrmgr/db.go managerTx.DeletePrivateKeys 66.04% (35/53) +github.com/conformal/btcwallet/waddrmgr/db.go openOrCreateDB 66.04% (35/53) +github.com/conformal/btcwallet/waddrmgr/manager.go Manager.ImportPrivateKey 64.71% (33/51) +github.com/conformal/btcwallet/waddrmgr/db.go managerTx.PutCryptoKeys 64.71% (11/17) +github.com/conformal/btcwallet/waddrmgr/manager.go Manager.loadAccountInfo 62.96% (34/54) +github.com/conformal/btcwallet/waddrmgr/db.go managerTx.FetchAccountInfo 61.54% (8/13) +github.com/conformal/btcwallet/waddrmgr/manager.go Manager.scriptAddressRowToManaged 60.00% (3/5) +github.com/conformal/btcwallet/waddrmgr/manager.go Create 58.59% (58/99) +github.com/conformal/btcwallet/waddrmgr/manager.go deriveAccountKey 53.85% (7/13) +github.com/conformal/btcwallet/waddrmgr/db.go managerDB.Update 50.00% (4/8) +github.com/conformal/btcwallet/waddrmgr/db.go managerDB.View 50.00% (4/8) +github.com/conformal/btcwallet/waddrmgr/db.go managerDB.Close 50.00% (2/4) +github.com/conformal/btcwallet/waddrmgr/db.go managerDB.CopyDB 45.45% (5/11) +github.com/conformal/btcwallet/waddrmgr/db.go managerTx.FetchAllAddresses 0.00% (0/20) +github.com/conformal/btcwallet/waddrmgr/manager.go Manager.AllActiveAddresses 0.00% (0/16) +github.com/conformal/btcwallet/waddrmgr/db.go managerDB.WriteTo 0.00% (0/11) +github.com/conformal/btcwallet/waddrmgr/sync.go BlockIterator.Next 0.00% (0/6) +github.com/conformal/btcwallet/waddrmgr/db.go managerTx.FetchNumAccounts 0.00% (0/6) +github.com/conformal/btcwallet/waddrmgr/manager.go Manager.Export 0.00% (0/3) +github.com/conformal/btcwallet/waddrmgr ----------------------------------- 72.59% (1030/1419) +