Provide new wallet address manager package.

This commit implements a new secure, scalable, hierarchical deterministic
wallet address manager package.

The following is an overview of features:

- BIP0032 hierarchical deterministic keys
- BIP0043/BIP0044 multi-account hierarchy
- Strong focus on security:
  - Fully encrypted database including public information such as
    addresses as well as private information such as private keys and
    scripts needed to redeem pay-to-script-hash transactions
  - Hardened against memory scraping through the use of actively clearing
    private material from memory when locked
  - Different crypto keys used for public, private, and script data
  - Ability for different passphrases for public and private data
  - Scrypt-based key derivation
  - NaCl-based secretbox cryptography (XSalsa20 and Poly1305)
  - Multi-tier scalable key design to allow instant password changes
    regardless of the number of addresses stored
- Import WIF keys
- Import pay-to-script-hash scripts for things such as multi-signature
  transactions
- Ability to export a watching-only version which does not contain any
  private key material
- Programmatically detectable errors, including encapsulation of errors
  from packages it relies on
- Address synchronization capabilities

This commit only provides the implementation package.  It does not
include integration into to the existing wallet code base or conversion of
existing addresses.  That functionality will be provided by future
commits.
This commit is contained in:
Dave Collins 2014-08-08 15:43:50 -05:00
parent 3f99ed233f
commit d0938d817f
15 changed files with 6582 additions and 0 deletions

238
snacl/snacl.go Normal file
View file

@ -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:
// <salt><digest><N><R><P>
//
// 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:
// <salt><digest><N><R><P>
//
// 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
}

112
snacl/snacl_test.go Normal file
View file

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

62
waddrmgr/README.md Normal file
View file

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

506
waddrmgr/address.go Normal file
View file

@ -0,0 +1,506 @@
/*
* Copyright (c) 2014 Conformal Systems LLC <info@conformal.com>
*
* 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
}

72
waddrmgr/common_test.go Normal file
View file

@ -0,0 +1,72 @@
/*
* Copyright (c) 2014 Conformal Systems LLC <info@conformal.com>
*
* 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}%<Z]fuGpbN[ZI")
privPassphrase2 = []byte("~{<]08%6!-?2s<$(8$8:f(5[4/!/{Y")
)
// checkManagerError ensures the passed error is a ManagerError with an error
// code that matches the passed error code.
func checkManagerError(t *testing.T, testName string, gotErr error, wantErrCode waddrmgr.ErrorCode) bool {
merr, ok := gotErr.(waddrmgr.ManagerError)
if !ok {
t.Errorf("%s: unexpected error type - got %T, want %T",
testName, gotErr, waddrmgr.ManagerError{})
return false
}
if merr.ErrorCode != wantErrCode {
t.Errorf("%s: unexpected error code - got %s, want %s",
testName, merr.ErrorCode, wantErrCode)
return false
}
return true
}
// hexToBytes is a wrapper around hex.DecodeString that panics if there is an
// error. It MUST only be used with hard coded values in the tests.
func hexToBytes(origHex string) []byte {
buf, err := hex.DecodeString(origHex)
if err != nil {
panic(err)
}
return buf
}
func init() {
// Tune the scrypt params down for tests so they execute quickly.
waddrmgr.TstSetScryptParams(16, 8, 1)
}

17
waddrmgr/cov_report.sh Normal file
View file

@ -0,0 +1,17 @@
#!/bin/sh
# This script uses gocov to generate a test coverage report.
# The gocov tool my be obtained with the following command:
# go get github.com/axw/gocov/gocov
#
# It will be installed to $GOPATH/bin, so ensure that location is in your $PATH.
# Check for gocov.
type gocov >/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

1356
waddrmgr/db.go Normal file

File diff suppressed because it is too large Load diff

167
waddrmgr/doc.go Normal file
View file

@ -0,0 +1,167 @@
/*
* Copyright (c) 2014 Conformal Systems LLC <info@conformal.com>
*
* 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

182
waddrmgr/error.go Normal file
View file

@ -0,0 +1,182 @@
/*
* Copyright (c) 2014 Conformal Systems LLC <info@conformal.com>
*
* 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}
}

119
waddrmgr/error_test.go Normal file
View file

@ -0,0 +1,119 @@
/*
* Copyright (c) 2014 Conformal Systems LLC <info@conformal.com>
*
* 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
}
}
}

63
waddrmgr/internal_test.go Normal file
View file

@ -0,0 +1,63 @@
/*
* Copyright (c) 2014 Conformal Systems LLC <info@conformal.com>
*
* 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
}

1821
waddrmgr/manager.go Normal file

File diff suppressed because it is too large Load diff

1500
waddrmgr/manager_test.go Normal file

File diff suppressed because it is too large Load diff

241
waddrmgr/sync.go Normal file
View file

@ -0,0 +1,241 @@
/*
* Copyright (c) 2014 Conformal Systems LLC <info@conformal.com>
*
* 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
}

126
waddrmgr/test_coverage.txt Normal file
View file

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