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:
parent
3f99ed233f
commit
d0938d817f
15 changed files with 6582 additions and 0 deletions
238
snacl/snacl.go
Normal file
238
snacl/snacl.go
Normal 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
112
snacl/snacl_test.go
Normal 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
62
waddrmgr/README.md
Normal 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
506
waddrmgr/address.go
Normal 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
72
waddrmgr/common_test.go
Normal 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
17
waddrmgr/cov_report.sh
Normal 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
1356
waddrmgr/db.go
Normal file
File diff suppressed because it is too large
Load diff
167
waddrmgr/doc.go
Normal file
167
waddrmgr/doc.go
Normal 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
182
waddrmgr/error.go
Normal 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
119
waddrmgr/error_test.go
Normal 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
63
waddrmgr/internal_test.go
Normal 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
1821
waddrmgr/manager.go
Normal file
File diff suppressed because it is too large
Load diff
1500
waddrmgr/manager_test.go
Normal file
1500
waddrmgr/manager_test.go
Normal file
File diff suppressed because it is too large
Load diff
241
waddrmgr/sync.go
Normal file
241
waddrmgr/sync.go
Normal 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
126
waddrmgr/test_coverage.txt
Normal 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)
|
||||
|
Loading…
Add table
Reference in a new issue