Generate new addresses from pubkeys if keypool is depleted.
This change uses the ChainedPubKey function to create addresses from the previous address's public key and the wallet chaincode, without the need for the private key, if the keypool has been depleted and the wallet is locked. This is done since the next chained private key is unsolvable without a locked wallet. If a wallet contains any of these chained addresses with missing private keys, the private keys are created during the next wallet unlock, using ChainedPrivKey.
This commit is contained in:
parent
be4c549682
commit
311d6176a8
2 changed files with 426 additions and 83 deletions
351
wallet/wallet.go
351
wallet/wallet.go
|
@ -32,7 +32,6 @@ import (
|
||||||
"github.com/conformal/btcec"
|
"github.com/conformal/btcec"
|
||||||
"github.com/conformal/btcutil"
|
"github.com/conformal/btcutil"
|
||||||
"github.com/conformal/btcwire"
|
"github.com/conformal/btcwire"
|
||||||
"github.com/davecgh/go-spew/spew"
|
|
||||||
"io"
|
"io"
|
||||||
"math/big"
|
"math/big"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -492,15 +491,16 @@ type Wallet struct {
|
||||||
sync.Mutex
|
sync.Mutex
|
||||||
key []byte
|
key []byte
|
||||||
}
|
}
|
||||||
chainIdxMap map[int64]*btcutil.AddressPubKeyHash
|
chainIdxMap map[int64]*btcutil.AddressPubKeyHash
|
||||||
importedAddrs []*btcAddress
|
importedAddrs []*btcAddress
|
||||||
lastChainIdx int64
|
lastChainIdx int64
|
||||||
|
missingKeysStart int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewWallet creates and initializes a new Wallet. name's and
|
// NewWallet creates and initializes a new Wallet. name's and
|
||||||
// desc's binary representation must not exceed 32 and 256 bytes,
|
// desc's binary representation must not exceed 32 and 256 bytes,
|
||||||
// respectively. All address private keys are encrypted with passphrase.
|
// respectively. All address private keys are encrypted with passphrase.
|
||||||
// The wallet is returned unlocked.
|
// The wallet is returned locked.
|
||||||
func NewWallet(name, desc string, passphrase []byte, net btcwire.BitcoinNet,
|
func NewWallet(name, desc string, passphrase []byte, net btcwire.BitcoinNet,
|
||||||
createdAt *BlockStamp, keypoolSize uint) (*Wallet, error) {
|
createdAt *BlockStamp, keypoolSize uint) (*Wallet, error) {
|
||||||
|
|
||||||
|
@ -676,6 +676,14 @@ func (w *Wallet) ReadFrom(r io.Reader) (n int64, err error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the private keys have not ben created yet, mark the
|
||||||
|
// earliest so all can be created on next wallet unlock.
|
||||||
|
if e.addr.flags.createPrivKeyNextUnlock {
|
||||||
|
if w.missingKeysStart < e.addr.chainIndex {
|
||||||
|
w.missingKeysStart = e.addr.chainIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
case *addrCommentEntry:
|
case *addrCommentEntry:
|
||||||
addr := e.address(w.net)
|
addr := e.address(w.net)
|
||||||
w.addrCommentMap[*addr] = comment(e.comment)
|
w.addrCommentMap[*addr] = comment(e.comment)
|
||||||
|
@ -767,7 +775,9 @@ func (w *Wallet) WriteTo(wtr io.Writer) (n int64, err error) {
|
||||||
// Unlock derives an AES key from passphrase and wallet's KDF
|
// Unlock derives an AES key from passphrase and wallet's KDF
|
||||||
// parameters and unlocks the root key of the wallet. If
|
// parameters and unlocks the root key of the wallet. If
|
||||||
// the unlock was successful, the wallet's secret key is saved,
|
// the unlock was successful, the wallet's secret key is saved,
|
||||||
// allowing the decryption of any encrypted private key.
|
// allowing the decryption of any encrypted private key. Any
|
||||||
|
// addresses created while the wallet was locked without private
|
||||||
|
// keys are created at this time.
|
||||||
func (w *Wallet) Unlock(passphrase []byte) error {
|
func (w *Wallet) Unlock(passphrase []byte) error {
|
||||||
// Derive key from KDF parameters and passphrase.
|
// Derive key from KDF parameters and passphrase.
|
||||||
key := Key(passphrase, &w.kdfParams)
|
key := Key(passphrase, &w.kdfParams)
|
||||||
|
@ -777,11 +787,15 @@ func (w *Wallet) Unlock(passphrase []byte) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If unlock was successful, save the secret key.
|
// If unlock was successful, create a copy for below and save the
|
||||||
|
// secret key.
|
||||||
|
keycopy := make([]byte, len(key))
|
||||||
|
copy(keycopy, key)
|
||||||
w.secret.Lock()
|
w.secret.Lock()
|
||||||
w.secret.key = key
|
w.secret.key = key
|
||||||
w.secret.Unlock()
|
w.secret.Unlock()
|
||||||
return nil
|
|
||||||
|
return w.createMissingPrivateKeys(keycopy)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lock performs a best try effort to remove and zero all secret keys
|
// Lock performs a best try effort to remove and zero all secret keys
|
||||||
|
@ -799,10 +813,8 @@ func (w *Wallet) Lock() (err error) {
|
||||||
|
|
||||||
// Remove clear text private keys from all address entries.
|
// Remove clear text private keys from all address entries.
|
||||||
for _, addr := range w.addrMap {
|
for _, addr := range w.addrMap {
|
||||||
addr.privKeyCT.Lock()
|
zero(addr.privKeyCT)
|
||||||
zero(addr.privKeyCT.key)
|
addr.privKeyCT = nil
|
||||||
addr.privKeyCT.key = nil
|
|
||||||
addr.privKeyCT.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
|
@ -828,37 +840,49 @@ func (w *Wallet) Version() (string, int) {
|
||||||
return "", 0
|
return "", 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// NextChainedAddress attempts to get the next chained address,
|
// NextChainedAddress attempts to get the next chained address.
|
||||||
// refilling the keypool if necessary.
|
// If there are addresses available in the keypool, the next address
|
||||||
|
// is used. If not and the wallet is unlocked, the keypool is extended.
|
||||||
|
// If locked, a new address's pubkey is chained off the last pubkey
|
||||||
|
// and added to the wallet.
|
||||||
func (w *Wallet) NextChainedAddress(bs *BlockStamp,
|
func (w *Wallet) NextChainedAddress(bs *BlockStamp,
|
||||||
keypoolSize uint) (*btcutil.AddressPubKeyHash, error) {
|
keypoolSize uint) (*btcutil.AddressPubKeyHash, error) {
|
||||||
|
|
||||||
// Attempt to get address hash of next chained address.
|
// Attempt to get address hash of next chained address.
|
||||||
next160, ok := w.chainIdxMap[w.highestUsed+1]
|
nextAPKH, ok := w.chainIdxMap[w.highestUsed+1]
|
||||||
if !ok {
|
if !ok {
|
||||||
// Extending the keypool requires an unlocked wallet.
|
// Extending the keypool requires an unlocked wallet.
|
||||||
aeskey := make([]byte, 32)
|
var aeskey []byte
|
||||||
w.secret.Lock()
|
w.secret.Lock()
|
||||||
if len(w.secret.key) != 32 {
|
if len(w.secret.key) == 32 {
|
||||||
|
// Key is available, make a copy and extend
|
||||||
|
// keypool.
|
||||||
|
aeskey = make([]byte, 32)
|
||||||
|
copy(aeskey, w.secret.key)
|
||||||
w.secret.Unlock()
|
w.secret.Unlock()
|
||||||
return nil, ErrWalletLocked
|
|
||||||
}
|
|
||||||
copy(aeskey, w.secret.key)
|
|
||||||
w.secret.Unlock()
|
|
||||||
|
|
||||||
err := w.extendKeypool(keypoolSize, aeskey, bs)
|
err := w.extendKeypool(keypoolSize, aeskey, bs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
w.secret.Unlock()
|
||||||
|
|
||||||
|
err := w.extendLockedWallet(bs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
next160, ok = w.chainIdxMap[w.highestUsed+1]
|
// Should be added to the internal maps, try lookup again.
|
||||||
|
nextAPKH, ok = w.chainIdxMap[w.highestUsed+1]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.New("chain index map inproperly updated")
|
return nil, errors.New("chain index map inproperly updated")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Look up address.
|
// Look up address.
|
||||||
addr, ok := w.addrMap[*next160]
|
addr, ok := w.addrMap[*nextAPKH]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.New("cannot find generated address")
|
return nil, errors.New("cannot find generated address")
|
||||||
}
|
}
|
||||||
|
@ -883,8 +907,6 @@ func (w *Wallet) extendKeypool(n uint, aeskey []byte, bs *BlockStamp) error {
|
||||||
a := w.chainIdxMap[w.lastChainIdx]
|
a := w.chainIdxMap[w.lastChainIdx]
|
||||||
addr, ok := w.addrMap[*a]
|
addr, ok := w.addrMap[*a]
|
||||||
if !ok {
|
if !ok {
|
||||||
spew.Dump(a)
|
|
||||||
spew.Dump(w.addrMap)
|
|
||||||
return errors.New("expected last chained address not found")
|
return errors.New("expected last chained address not found")
|
||||||
}
|
}
|
||||||
privkey, err := addr.unlock(aeskey)
|
privkey, err := addr.unlock(aeskey)
|
||||||
|
@ -924,6 +946,90 @@ func (w *Wallet) extendKeypool(n uint, aeskey []byte, bs *BlockStamp) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// extendLockedWallet creates one new address without a private key
|
||||||
|
// (allowing for extending the address chain from a locked wallet)
|
||||||
|
// chained from the last used chained address and adds the address to
|
||||||
|
// the wallet's internal bookkeeping structures. This function should
|
||||||
|
// not be called unless the keypool has been depleted.
|
||||||
|
func (w *Wallet) extendLockedWallet(bs *BlockStamp) error {
|
||||||
|
a := w.chainIdxMap[w.lastChainIdx]
|
||||||
|
addr, ok := w.addrMap[*a]
|
||||||
|
if !ok {
|
||||||
|
return errors.New("expected last chained address not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
cc := addr.chaincode[:]
|
||||||
|
prevPubkey := addr.pubKey
|
||||||
|
|
||||||
|
nextPubkey, err := ChainedPubKey(prevPubkey, cc)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
newaddr, err := newBtcAddressWithoutPrivkey(nextPubkey, nil, bs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a = newaddr.address(w.net)
|
||||||
|
w.addrMap[*a] = newaddr
|
||||||
|
newaddr.chainIndex = addr.chainIndex + 1
|
||||||
|
w.chainIdxMap[newaddr.chainIndex] = a
|
||||||
|
w.lastChainIdx++
|
||||||
|
copy(newaddr.chaincode[:], cc)
|
||||||
|
|
||||||
|
if w.missingKeysStart == 0 {
|
||||||
|
w.missingKeysStart = newaddr.chainIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Wallet) createMissingPrivateKeys(aeskey []byte) error {
|
||||||
|
idx := w.missingKeysStart
|
||||||
|
if idx == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup previous address.
|
||||||
|
apkh, ok := w.chainIdxMap[idx-1]
|
||||||
|
if !ok {
|
||||||
|
return errors.New("missing previous chained address")
|
||||||
|
}
|
||||||
|
prevAddr := w.addrMap[*apkh]
|
||||||
|
prevPrivKey, err := prevAddr.unlock(aeskey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := idx; ; i++ {
|
||||||
|
// Get the next private key for the ith address in the address chain.
|
||||||
|
ithPrivKey, err := ChainedPrivKey(prevPrivKey, prevAddr.pubKey,
|
||||||
|
prevAddr.chaincode[:])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the address with the missing private key, set, and
|
||||||
|
// encrypt.
|
||||||
|
apkh, ok := w.chainIdxMap[i]
|
||||||
|
if !ok {
|
||||||
|
// Finished.
|
||||||
|
break
|
||||||
|
}
|
||||||
|
addr := w.addrMap[*apkh]
|
||||||
|
addr.privKeyCT = ithPrivKey
|
||||||
|
if err := addr.encrypt(aeskey); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set previous address and private key for next iteration.
|
||||||
|
prevAddr = addr
|
||||||
|
prevPrivKey = ithPrivKey
|
||||||
|
}
|
||||||
|
|
||||||
|
w.missingKeysStart = 0
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// AddressKey returns the private key for a payment address stored
|
// AddressKey returns the private key for a payment address stored
|
||||||
// in a wallet. This can fail if the payment address is for a different
|
// in a wallet. This can fail if the payment address is for a different
|
||||||
// Bitcoin network than what this wallet uses, the address is not
|
// Bitcoin network than what this wallet uses, the address is not
|
||||||
|
@ -1217,6 +1323,52 @@ func (w *Wallet) ActiveAddresses() map[btcutil.Address]*AddressInfo {
|
||||||
return addrs
|
return addrs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// unlockAddress decrypts and stores a pointer to an address's private
|
||||||
|
// key, failing if the address is not encrypted, or the provided key is
|
||||||
|
// incorrect. If the requested address's private key has not yet been
|
||||||
|
// saved, the previous chained address is looked up and the private key
|
||||||
|
// is saved and encrypted. 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 (w *Wallet) unlockAddress(a *btcAddress, key []byte) (privKeyCT []byte, err error) {
|
||||||
|
if !a.flags.encrypted {
|
||||||
|
return nil, errors.New("unable to unlock unencrypted address")
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.flags.createPrivKeyNextUnlock {
|
||||||
|
// Look up previous chained address and unlock its private key.
|
||||||
|
prevAPKH, ok := w.chainIdxMap[a.chainIndex-1]
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("cannot determine previous address to create privkey")
|
||||||
|
}
|
||||||
|
prevAddr := w.addrMap[*prevAPKH]
|
||||||
|
prevPrivKey, err := w.unlockAddress(prevAddr, key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate this address's private key.
|
||||||
|
privkey, err := ChainedPrivKey(prevPrivKey, prevAddr.pubKey,
|
||||||
|
prevAddr.chaincode[:])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
a.privKeyCT = privkey
|
||||||
|
|
||||||
|
// Encrypt clear text private key.
|
||||||
|
if err := a.encrypt(key); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
a.flags.hasPrivKey = true
|
||||||
|
a.flags.createPrivKeyNextUnlock = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.unlock(key)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
type walletFlags struct {
|
type walletFlags struct {
|
||||||
useEncryption bool
|
useEncryption bool
|
||||||
watchingOnly bool
|
watchingOnly bool
|
||||||
|
@ -1249,36 +1401,32 @@ type addrFlags struct {
|
||||||
compressed bool
|
compressed bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (af *addrFlags) ReadFrom(r io.Reader) (n int64, err error) {
|
func (af *addrFlags) ReadFrom(r io.Reader) (int64, error) {
|
||||||
var read int64
|
|
||||||
var b [8]byte
|
var b [8]byte
|
||||||
read, err = binaryRead(r, binary.LittleEndian, &b)
|
n, err := r.Read(b[:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return n + read, err
|
return int64(n), err
|
||||||
}
|
|
||||||
n += read
|
|
||||||
|
|
||||||
if b[0]&(1<<0) != 0 {
|
|
||||||
af.hasPrivKey = true
|
|
||||||
}
|
|
||||||
if b[0]&(1<<1) != 0 {
|
|
||||||
af.hasPubKey = true
|
|
||||||
}
|
|
||||||
if b[0]&(1<<2) == 0 {
|
|
||||||
return n, errors.New("address flag specifies unencrypted address")
|
|
||||||
}
|
|
||||||
af.encrypted = true
|
|
||||||
if b[0]&(1<<3) != 0 {
|
|
||||||
af.createPrivKeyNextUnlock = true
|
|
||||||
}
|
|
||||||
if b[0]&(1<<4) != 0 {
|
|
||||||
af.compressed = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return n, nil
|
af.hasPrivKey = b[0]&(1<<0) != 0
|
||||||
|
af.hasPubKey = b[0]&(1<<1) != 0
|
||||||
|
af.encrypted = b[0]&(1<<2) != 0
|
||||||
|
af.createPrivKeyNextUnlock = b[0]&(1<<3) != 0
|
||||||
|
af.compressed = b[0]&(1<<4) != 0
|
||||||
|
|
||||||
|
// Currently (at least until watching-only wallets are implemented)
|
||||||
|
// btcwallet shall refuse to open any unencrypted addresses. This
|
||||||
|
// check only makes sense if there is a private key to encrypt, which
|
||||||
|
// there may not be if the keypool was extended from just the last
|
||||||
|
// public key and no private keys were written.
|
||||||
|
if af.hasPrivKey && !af.encrypted {
|
||||||
|
return int64(n), errors.New("private key is unencrypted")
|
||||||
|
}
|
||||||
|
|
||||||
|
return int64(n), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (af *addrFlags) WriteTo(w io.Writer) (n int64, err error) {
|
func (af *addrFlags) WriteTo(w io.Writer) (int64, error) {
|
||||||
var b [8]byte
|
var b [8]byte
|
||||||
if af.hasPrivKey {
|
if af.hasPrivKey {
|
||||||
b[0] |= 1 << 0
|
b[0] |= 1 << 0
|
||||||
|
@ -1286,11 +1434,13 @@ func (af *addrFlags) WriteTo(w io.Writer) (n int64, err error) {
|
||||||
if af.hasPubKey {
|
if af.hasPubKey {
|
||||||
b[0] |= 1 << 1
|
b[0] |= 1 << 1
|
||||||
}
|
}
|
||||||
if !af.encrypted {
|
if af.hasPrivKey && !af.encrypted {
|
||||||
// We only support encrypted privkeys.
|
// We only support encrypted privkeys.
|
||||||
return n, errors.New("address must be encrypted")
|
return 0, errors.New("address must be encrypted")
|
||||||
|
}
|
||||||
|
if af.encrypted {
|
||||||
|
b[0] |= 1 << 2
|
||||||
}
|
}
|
||||||
b[0] |= 1 << 2
|
|
||||||
if af.createPrivKeyNextUnlock {
|
if af.createPrivKeyNextUnlock {
|
||||||
b[0] |= 1 << 3
|
b[0] |= 1 << 3
|
||||||
}
|
}
|
||||||
|
@ -1298,7 +1448,8 @@ func (af *addrFlags) WriteTo(w io.Writer) (n int64, err error) {
|
||||||
b[0] |= 1 << 4
|
b[0] |= 1 << 4
|
||||||
}
|
}
|
||||||
|
|
||||||
return binaryWrite(w, binary.LittleEndian, b)
|
n, err := w.Write(b[:])
|
||||||
|
return int64(n), err
|
||||||
}
|
}
|
||||||
|
|
||||||
// recentBlocks holds at most the last 20 seen block hashes as well as
|
// recentBlocks holds at most the last 20 seen block hashes as well as
|
||||||
|
@ -1572,10 +1723,7 @@ type btcAddress struct {
|
||||||
lastSeen int64
|
lastSeen int64
|
||||||
firstBlock int32
|
firstBlock int32
|
||||||
lastBlock int32
|
lastBlock int32
|
||||||
privKeyCT struct {
|
privKeyCT []byte // non-nil if unlocked.
|
||||||
sync.Mutex
|
|
||||||
key []byte // non-nil if unlocked.
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -1654,14 +1802,16 @@ func newBtcAddress(privkey, iv []byte, bs *BlockStamp, compressed bool) (addr *b
|
||||||
|
|
||||||
addr = &btcAddress{
|
addr = &btcAddress{
|
||||||
flags: addrFlags{
|
flags: addrFlags{
|
||||||
hasPrivKey: true,
|
hasPrivKey: true,
|
||||||
hasPubKey: true,
|
hasPubKey: true,
|
||||||
compressed: compressed,
|
createPrivKeyNextUnlock: false,
|
||||||
|
compressed: compressed,
|
||||||
|
encrypted: false, // will be, but isn't yet.
|
||||||
},
|
},
|
||||||
firstSeen: time.Now().Unix(),
|
firstSeen: time.Now().Unix(),
|
||||||
firstBlock: bs.Height,
|
firstBlock: bs.Height,
|
||||||
}
|
}
|
||||||
addr.privKeyCT.key = privkey
|
addr.privKeyCT = privkey
|
||||||
copy(addr.initVector[:], iv)
|
copy(addr.initVector[:], iv)
|
||||||
addr.pubKey = pubkeyFromPrivkey(privkey, compressed)
|
addr.pubKey = pubkeyFromPrivkey(privkey, compressed)
|
||||||
copy(addr.pubKeyHash[:], btcutil.Hash160(addr.pubKey))
|
copy(addr.pubKeyHash[:], btcutil.Hash160(addr.pubKey))
|
||||||
|
@ -1669,6 +1819,49 @@ func newBtcAddress(privkey, iv []byte, bs *BlockStamp, compressed bool) (addr *b
|
||||||
return addr, nil
|
return addr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// newBtcAddressWithoutPrivkey initializes and returns a new address with an
|
||||||
|
// unknown (at the time) private key that must be found later. pubkey must be
|
||||||
|
// 33 or 65 bytes, and iv must be 16 bytes or empty (in which case it is
|
||||||
|
// randomly generated).
|
||||||
|
func newBtcAddressWithoutPrivkey(pubkey, iv []byte, bs *BlockStamp) (addr *btcAddress, err error) {
|
||||||
|
var compressed bool
|
||||||
|
switch len(pubkey) {
|
||||||
|
case 33:
|
||||||
|
compressed = true
|
||||||
|
|
||||||
|
case 65:
|
||||||
|
compressed = false
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, errors.New("incorrect pubkey length")
|
||||||
|
}
|
||||||
|
if len(iv) == 0 {
|
||||||
|
iv = make([]byte, 16)
|
||||||
|
if _, err := rand.Read(iv); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else if len(iv) != 16 {
|
||||||
|
return nil, errors.New("init vector must be nil or 16 bytes large")
|
||||||
|
}
|
||||||
|
|
||||||
|
addr = &btcAddress{
|
||||||
|
flags: addrFlags{
|
||||||
|
hasPrivKey: false,
|
||||||
|
hasPubKey: true,
|
||||||
|
createPrivKeyNextUnlock: true,
|
||||||
|
compressed: compressed,
|
||||||
|
encrypted: false,
|
||||||
|
},
|
||||||
|
firstSeen: time.Now().Unix(),
|
||||||
|
firstBlock: bs.Height,
|
||||||
|
}
|
||||||
|
copy(addr.initVector[:], iv)
|
||||||
|
addr.pubKey = pubkey
|
||||||
|
copy(addr.pubKeyHash[:], btcutil.Hash160(pubkey))
|
||||||
|
|
||||||
|
return addr, nil
|
||||||
|
}
|
||||||
|
|
||||||
// newRootBtcAddress generates a new address, also setting the
|
// newRootBtcAddress generates a new address, also setting the
|
||||||
// chaincode and chain index to represent this address as a root
|
// chaincode and chain index to represent this address as a root
|
||||||
// address.
|
// address.
|
||||||
|
@ -1704,13 +1897,13 @@ func (a *btcAddress) verifyKeypairs() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(a.privKeyCT.key) != 32 {
|
if len(a.privKeyCT) != 32 {
|
||||||
return errors.New("private key unavailable")
|
return errors.New("private key unavailable")
|
||||||
}
|
}
|
||||||
|
|
||||||
privkey := &ecdsa.PrivateKey{
|
privkey := &ecdsa.PrivateKey{
|
||||||
PublicKey: *pubkey,
|
PublicKey: *pubkey,
|
||||||
D: new(big.Int).SetBytes(a.privKeyCT.key),
|
D: new(big.Int).SetBytes(a.privKeyCT),
|
||||||
}
|
}
|
||||||
|
|
||||||
data := "String to sign."
|
data := "String to sign."
|
||||||
|
@ -1834,9 +2027,7 @@ func (a *btcAddress) encrypt(key []byte) error {
|
||||||
if a.flags.encrypted {
|
if a.flags.encrypted {
|
||||||
return errors.New("address already encrypted")
|
return errors.New("address already encrypted")
|
||||||
}
|
}
|
||||||
a.privKeyCT.Lock()
|
if len(a.privKeyCT) != 32 {
|
||||||
defer a.privKeyCT.Unlock()
|
|
||||||
if len(a.privKeyCT.key) != 32 {
|
|
||||||
return errors.New("invalid clear text private key")
|
return errors.New("invalid clear text private key")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1846,8 +2037,9 @@ func (a *btcAddress) encrypt(key []byte) error {
|
||||||
}
|
}
|
||||||
aesEncrypter := cipher.NewCFBEncrypter(aesBlockEncrypter, a.initVector[:])
|
aesEncrypter := cipher.NewCFBEncrypter(aesBlockEncrypter, a.initVector[:])
|
||||||
|
|
||||||
aesEncrypter.XORKeyStream(a.privKey[:], a.privKeyCT.key)
|
aesEncrypter.XORKeyStream(a.privKey[:], a.privKeyCT)
|
||||||
|
|
||||||
|
a.flags.hasPrivKey = true
|
||||||
a.flags.encrypted = true
|
a.flags.encrypted = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -1859,14 +2051,12 @@ func (a *btcAddress) lock() error {
|
||||||
return errors.New("unable to lock unencrypted address")
|
return errors.New("unable to lock unencrypted address")
|
||||||
}
|
}
|
||||||
|
|
||||||
a.privKeyCT.Lock()
|
zero(a.privKeyCT)
|
||||||
zero(a.privKeyCT.key)
|
a.privKeyCT = nil
|
||||||
a.privKeyCT.key = nil
|
|
||||||
a.privKeyCT.Unlock()
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// unlock decrypts and stores a pointer to this address's private key,
|
// unlock decrypts and stores a pointer to an address's private key,
|
||||||
// failing if the address is not encrypted, or the provided key is
|
// failing if the address is not encrypted, or the provided key is
|
||||||
// incorrect. The returned clear text private key will always be a copy
|
// incorrect. The returned clear text private key will always be a copy
|
||||||
// that may be safely used by the caller without worrying about it being
|
// that may be safely used by the caller without worrying about it being
|
||||||
|
@ -1878,14 +2068,11 @@ func (a *btcAddress) unlock(key []byte) (privKeyCT []byte, err error) {
|
||||||
|
|
||||||
// If secret is already saved, return a copy without performing a full
|
// If secret is already saved, return a copy without performing a full
|
||||||
// unlock.
|
// unlock.
|
||||||
a.privKeyCT.Lock()
|
if len(a.privKeyCT) == 32 {
|
||||||
if len(a.privKeyCT.key) == 32 {
|
|
||||||
privKeyCT := make([]byte, 32)
|
privKeyCT := make([]byte, 32)
|
||||||
copy(privKeyCT, a.privKeyCT.key)
|
copy(privKeyCT, a.privKeyCT)
|
||||||
a.privKeyCT.Unlock()
|
|
||||||
return privKeyCT, nil
|
return privKeyCT, nil
|
||||||
}
|
}
|
||||||
a.privKeyCT.Unlock()
|
|
||||||
|
|
||||||
// Decrypt private key with AES key.
|
// Decrypt private key with AES key.
|
||||||
aesBlockDecrypter, err := aes.NewCipher(key)
|
aesBlockDecrypter, err := aes.NewCipher(key)
|
||||||
|
@ -1909,9 +2096,7 @@ func (a *btcAddress) unlock(key []byte) (privKeyCT []byte, err error) {
|
||||||
|
|
||||||
privkeyCopy := make([]byte, 32)
|
privkeyCopy := make([]byte, 32)
|
||||||
copy(privkeyCopy, privkey)
|
copy(privkeyCopy, privkey)
|
||||||
a.privKeyCT.Lock()
|
a.privKeyCT = privkey
|
||||||
a.privKeyCT.key = privkey
|
|
||||||
a.privKeyCT.Unlock()
|
|
||||||
return privkeyCopy, nil
|
return privkeyCopy, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
"github.com/conformal/btcec"
|
"github.com/conformal/btcec"
|
||||||
"github.com/conformal/btcwire"
|
"github.com/conformal/btcwire"
|
||||||
"github.com/davecgh/go-spew/spew"
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
@ -304,3 +305,160 @@ func TestChaining(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWalletPubkeyChaining(t *testing.T) {
|
||||||
|
// Set a reasonable keypool size that isn't too big nor too small for testing.
|
||||||
|
const keypoolSize = 5
|
||||||
|
|
||||||
|
w, err := NewWallet("banana wallet", "A wallet for testing.",
|
||||||
|
[]byte("banana"), btcwire.MainNet, &BlockStamp{}, keypoolSize)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("Error creating new wallet: " + err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !w.IsLocked() {
|
||||||
|
t.Error("New wallet is not locked.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wallet should have a total of 6 addresses, one for the root, plus 5 in
|
||||||
|
// the keypool with their private keys set. Ask for as many new addresses
|
||||||
|
// as needed to deplete the pool.
|
||||||
|
for i := 0; i < keypoolSize; i++ {
|
||||||
|
_, err := w.NextChainedAddress(&BlockStamp{}, keypoolSize)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error getting next address from keypool: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get next chained address after depleting the keypool. This will extend
|
||||||
|
// the chain based on the last pubkey, not privkey.
|
||||||
|
addrWithoutPrivkey, err := w.NextChainedAddress(&BlockStamp{}, keypoolSize)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed to extend address chain from pubkey: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup address info. This should succeed even without the private
|
||||||
|
// key available.
|
||||||
|
info, err := w.AddressInfo(addrWithoutPrivkey)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed to get info about address without private key: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// sanity checks
|
||||||
|
if !info.Compressed {
|
||||||
|
t.Errorf("Pubkey should be compressed.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if info.Imported {
|
||||||
|
t.Errorf("Should not be marked as imported.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to lookup it's private key. This should fail.
|
||||||
|
_, err = w.AddressKey(addrWithoutPrivkey)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Incorrectly returned nil error for looking up private key for address without one saved.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deserialize w and serialize into a new wallet. The rest of the checks
|
||||||
|
// in this test test against both a fresh, as well as an "opened and closed"
|
||||||
|
// wallet with the missing private key.
|
||||||
|
serializedWallet := new(bytes.Buffer)
|
||||||
|
_, err = w.WriteTo(serializedWallet)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error writing wallet with missing private key: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w2 := new(Wallet)
|
||||||
|
_, err = w2.ReadFrom(serializedWallet)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error reading wallet with missing private key: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unlock wallet. This should trigger creating the private key for
|
||||||
|
// the address.
|
||||||
|
if err = w.Unlock([]byte("banana")); err != nil {
|
||||||
|
t.Errorf("Can't unlock original wallet: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = w2.Unlock([]byte("banana")); err != nil {
|
||||||
|
t.Errorf("Can't unlock re-read wallet: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same address, better variable name.
|
||||||
|
addrWithPrivKey := addrWithoutPrivkey
|
||||||
|
|
||||||
|
// Try a private key lookup again. The private key should now be available.
|
||||||
|
key1, err := w.AddressKey(addrWithPrivKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Private key for original wallet was not created! %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
key2, err := w2.AddressKey(addrWithPrivKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Private key for re-read wallet was not created! %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keys returned by both wallets must match.
|
||||||
|
if !reflect.DeepEqual(key1, key2) {
|
||||||
|
t.Errorf("Private keys for address originally created without one mismtach between original and re-read wallet.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign some data with the private key, then verify signature with the pubkey.
|
||||||
|
hash := []byte("hash to sign")
|
||||||
|
r, s, err := ecdsa.Sign(rand.Reader, key1, hash)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unable to sign hash with the created private key: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pubKeyStr, _ := hex.DecodeString(info.Pubkey)
|
||||||
|
pubKey, err := btcec.ParsePubKey(pubKeyStr, btcec.S256())
|
||||||
|
ok := ecdsa.Verify(pubKey, hash, r, s)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("ECDSA verification failed; address's pubkey mismatches the privkey.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that normal keypool extension and address creation continues to
|
||||||
|
// work. With the wallet still unlocked, create a new address. This
|
||||||
|
// will cause the keypool to refill and return the first address from the
|
||||||
|
// keypool.
|
||||||
|
nextAddr, err := w.NextChainedAddress(&BlockStamp{}, keypoolSize)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unable to create next address or refill keypool after finding the privkey: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nextInfo, err := w.AddressInfo(nextAddr)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Couldn't get info about the next address in the chain: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nextKey, err := w.AddressKey(nextAddr)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Couldn't get private key for the next address in the chain: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do an ECDSA signature check here as well, this time for the next
|
||||||
|
// address after the one made without the private key.
|
||||||
|
r, s, err = ecdsa.Sign(rand.Reader, nextKey, hash)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unable to sign hash with the created private key: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pubKeyStr, _ = hex.DecodeString(nextInfo.Pubkey)
|
||||||
|
pubKey, err = btcec.ParsePubKey(pubKeyStr, btcec.S256())
|
||||||
|
ok = ecdsa.Verify(pubKey, hash, r, s)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("ECDSA verification failed; next address's keypair does not match.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue