From 38ed238a7f0dec171e46b32fabc00b0e4218d1aa Mon Sep 17 00:00:00 2001 From: Josh Rickmar Date: Thu, 21 Nov 2013 11:57:28 -0500 Subject: [PATCH] Refill keypool if empty and wallet is unlocked. --- cmd.go | 2 +- cmdmgr.go | 29 +++++++++++- createtx.go | 10 ++++- wallet/wallet.go | 114 +++++++++++++++++++++++++++++++---------------- 4 files changed, 113 insertions(+), 42 deletions(-) diff --git a/cmd.go b/cmd.go index 7dc50d1..4bf78b1 100644 --- a/cmd.go +++ b/cmd.go @@ -334,7 +334,7 @@ func main() { accounts.Unlock() default: - log.Errorf("cannot open wallet: %v", err) + log.Warnf("cannot open wallet: %v", err) } // Read CA file to verify a btcd TLS connection. diff --git a/cmdmgr.go b/cmdmgr.go index dc383ea..7ec807a 100644 --- a/cmdmgr.go +++ b/cmdmgr.go @@ -388,8 +388,34 @@ func GetNewAddress(frontend chan []byte, icmd btcjson.Cmd) { return } + // Get current block's height and hash. + bs, err := GetCurBlock() + if err != nil { + e := &btcjson.Error{ + Code: btcjson.ErrInternal.Code, + Message: "btcd disconnected", + } + ReplyError(frontend, cmd.Id(), e) + return + } + // Get next address from wallet. - addr, err := a.NextUnusedAddress() + addr, err := a.NextChainedAddress(&bs) + if err == wallet.ErrWalletLocked { + // The wallet is locked error may be sent if the keypool needs + // to be refilled, but the wallet is currently in a locked + // state. Notify the frontend that an unlock is needed to + // refill the keypool. + ReplyError(frontend, cmd.Id(), &btcjson.ErrWalletKeypoolRanOut) + return + } else if err != nil { + e := &btcjson.Error{ + Code: btcjson.ErrWallet.Code, + Message: err.Error(), + } + ReplyError(frontend, cmd.Id(), e) + return + } if err != nil { // TODO(jrick): generate new addresses if the address pool is // empty. @@ -893,6 +919,7 @@ func WalletPassphrase(frontend chan []byte, icmd btcjson.Cmd) { &btcjson.ErrWalletPassphraseIncorrect) return } + // XXX ReplySuccess(frontend, cmd.Id(), nil) NotifyWalletLockStateChange("", false) go func() { diff --git a/createtx.go b/createtx.go index bf3eff9..a3c5b64 100644 --- a/createtx.go +++ b/createtx.go @@ -203,9 +203,15 @@ func (w *Account) txToPairs(pairs map[string]int64, fee int64, minconf int) (*Cr // Create a new address to spend leftover outputs to. // TODO(jrick): use the next chained address, not the next unused. var err error - changeAddr, err = w.NextUnusedAddress() + // Get current block's height and hash. + bs, err := GetCurBlock() if err != nil { - return nil, fmt.Errorf("failed to get next unused address: %s", err) + return nil, err + } + + changeAddr, err = w.NextChainedAddress(&bs) + if err != nil { + return nil, fmt.Errorf("failed to get next address: %s", err) } // Spend change diff --git a/wallet/wallet.go b/wallet/wallet.go index 738338a..bac924d 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -46,6 +46,9 @@ const ( // Maximum length in bytes of a comment that can have a size represented // as a uint16. maxCommentLen = (1 << 16) - 1 + + // Number of addresses to extend keypool by. + nKeypoolIncrement = 100 ) const ( @@ -409,9 +412,6 @@ func NewWallet(name, desc string, passphrase []byte, net btcwire.BitcoinNet, return nil, err } - // Define number of addresses to pre-generate for keypool. - const nPregenerated = 100 - // Create and fill wallet. w := &Wallet{ version: 0, // TODO(jrick): implement versioning @@ -432,7 +432,7 @@ func NewWallet(name, desc string, passphrase []byte, net btcwire.BitcoinNet, addrCommentMap: make(map[addressHashKey]comment), txCommentMap: make(map[transactionHashKey]comment), chainIdxMap: make(map[int64]addressHashKey), - lastChainIdx: nPregenerated - 1, + lastChainIdx: rootKeyChainIdx, } copy(w.name[:], []byte(name)) copy(w.desc[:], []byte(desc)) @@ -441,30 +441,9 @@ func NewWallet(name, desc string, passphrase []byte, net btcwire.BitcoinNet, w.addrMap[addressHashKey(w.keyGenerator.pubKeyHash[:])] = &w.keyGenerator w.chainIdxMap[rootKeyChainIdx] = addressHashKey(w.keyGenerator.pubKeyHash[:]) - // Pre-generate encrypted addresses and add to maps. - addr := &w.keyGenerator - cc := addr.chaincode[:] - for i := 0; i < nPregenerated; i++ { - // Wallet has not been returned to the caller yet, so need to - // lock and unlock the previous address's key's clear text - // private key mutex. - privkey, err := ChainedPrivKey(addr.privKeyCT.key, addr.pubKey, cc) - if err != nil { - return nil, err - } - newaddr, err := newBtcAddress(privkey, nil, createdAt, true) - if err != nil { - return nil, err - } - if err = newaddr.encrypt(aeskey); err != nil { - return nil, err - } - addrKey := addressHashKey(newaddr.pubKeyHash[:]) - w.addrMap[addrKey] = newaddr - newaddr.chainIndex = addr.chainIndex + 1 - w.chainIdxMap[newaddr.chainIndex] = addrKey - copy(newaddr.chaincode[:], cc) // armory does this.. but why? - addr = newaddr + // Fill keypool. + if err := w.extendKeypool(nKeypoolIncrement, aeskey, createdAt); err != nil { + return nil, err } return w, nil @@ -703,29 +682,88 @@ func (w *Wallet) Version() (string, int) { return "", 0 } -// NextUnusedAddress attempts to get the next chained address. -// -// TODO(jrick): this currently relies on pre-generated addresses -// and will return an empty string if the address pool has run out. -func (w *Wallet) NextUnusedAddress() (string, error) { +// NextChainedAddress attempts to get the next chained address, +// refilling the keypool if necessary. +func (w *Wallet) NextChainedAddress(bs *BlockStamp) (string, error) { // Attempt to get address hash of next chained address. next160, ok := w.chainIdxMap[w.highestUsed+1] if !ok { - // TODO(jrick): Re-fill key pool. - return "", errors.New("cannot find generated address") + // Extending the keypool requires an unlocked wallet. + aeskey := make([]byte, 32) + w.secret.Lock() + if len(w.secret.key) != 32 { + w.secret.Unlock() + return "", ErrWalletLocked + } + copy(aeskey, w.secret.key) + w.secret.Unlock() + + err := w.extendKeypool(nKeypoolIncrement, aeskey, bs) + if err != nil { + return "", err + } + + next160, ok = w.chainIdxMap[w.highestUsed+1] + if !ok { + return "", errors.New("chain index map inproperly updated") + } } - w.highestUsed++ // Look up address. - addr := w.addrMap[next160] - if addr == nil { + addr, ok := w.addrMap[next160] + if !ok { return "", errors.New("cannot find generated address") } + w.highestUsed++ + // Create and return payment address for address hash. return addr.paymentAddress(w.net) } +// extendKeypool grows the keypool by n addresses. +func (w *Wallet) extendKeypool(n uint, aeskey []byte, bs *BlockStamp) error { + // Get last chained address. New chained addresses will be + // chained off of this address's chaincode and private key. + addrKey := w.chainIdxMap[w.lastChainIdx] + addr, ok := w.addrMap[addrKey] + if !ok { + return errors.New("expected last chained address not found") + } + privkey, err := addr.unlock(aeskey) + if err != nil { + return err + } + cc := addr.chaincode[:] + + // Create n encrypted addresses and add each to the wallet's + // bookkeeping maps. + for i := uint(0); i < n; i++ { + privkey, err = ChainedPrivKey(privkey, addr.pubKey, cc) + if err != nil { + return err + } + newaddr, err := newBtcAddress(privkey, nil, bs, true) + if err != nil { + return err + } + if err = newaddr.encrypt(aeskey); err != nil { + return err + } + addrKey := addressHashKey(newaddr.pubKeyHash[:]) + w.addrMap[addrKey] = newaddr + newaddr.chainIndex = addr.chainIndex + 1 + w.chainIdxMap[newaddr.chainIndex] = addrKey + w.lastChainIdx++ + // armory does this.. but all the chaincodes are equal so why + // not use the root's? + copy(newaddr.chaincode[:], cc) + addr = newaddr + } + + return nil +} + // addrHashForAddress decodes and returns the address hash for a // payment address string, performing some basic sanity checking that it // matches the Bitcoin network used by the wallet.