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) +