diff --git a/account.go b/account.go index ed0a08e..e0cf06d 100644 --- a/account.go +++ b/account.go @@ -18,6 +18,7 @@ package main import ( "bytes" + "errors" "fmt" "github.com/conformal/btcjson" "github.com/conformal/btcutil" @@ -75,8 +76,8 @@ func NewAccountStore() *AccountStore { // TODO(jrick): This must also roll back the UTXO and TX stores, and notify // all wallets of new account balances. func (s *AccountStore) Rollback(height int32, hash *btcwire.ShaHash) { - for _, w := range s.m { - w.Rollback(height, hash) + for _, a := range s.m { + a.Rollback(height, hash) } } @@ -127,6 +128,94 @@ func (a *Account) CalculateBalance(confirms int) float64 { return float64(bal) / float64(btcutil.SatoshiPerBitcoin) } +// DumpPrivKeys returns the WIF-encoded private keys for all addresses +// non-watching addresses in a wallets. +func (a *Account) DumpPrivKeys() ([]string, error) { + a.mtx.RLock() + defer a.mtx.RUnlock() + + // Iterate over each active address, appending the private + // key to privkeys. + var privkeys []string + for _, addr := range a.GetActiveAddresses() { + key, err := a.GetAddressKey(addr.Address) + if err != nil { + return nil, err + } + encKey, err := btcutil.EncodePrivateKey(key.D.Bytes(), + a.Net(), addr.Compressed) + if err != nil { + return nil, err + } + privkeys = append(privkeys, encKey) + } + + return privkeys, nil +} + +// DumpWIFPrivateKey returns the WIF encoded private key for a +// single wallet address. +func (a *Account) DumpWIFPrivateKey(address string) (string, error) { + a.mtx.RLock() + defer a.mtx.RUnlock() + + // Get private key from wallet if it exists. + key, err := a.GetAddressKey(address) + if err != nil { + return "", err + } + + // Get address info. This is needed to determine whether + // the pubkey is compressed or not. + info, err := a.GetAddressInfo(address) + if err != nil { + return "", err + } + + // Return WIF-encoding of the private key. + return btcutil.EncodePrivateKey(key.D.Bytes(), a.Net(), info.Compressed) +} + +// ImportWIFPrivateKey takes a WIF encoded private key and adds it to the +// wallet. If the import is successful, the payment address string is +// returned. +func (a *Account) ImportWIFPrivateKey(wif, label string, + bs *wallet.BlockStamp) (string, error) { + + // Decode WIF private key and perform sanity checking. + privkey, net, compressed, err := btcutil.DecodePrivateKey(wif) + if err != nil { + return "", err + } + if net != a.Net() { + return "", errors.New("wrong network") + } + + // Attempt to import private key into wallet. + a.mtx.Lock() + addr, err := a.ImportPrivateKey(privkey, compressed, bs) + if err != nil { + a.mtx.Unlock() + return "", err + } + + // Immediately write dirty wallet to disk. + // + // TODO(jrick): change writeDirtyToDisk to not grab the writer lock. + // Don't want to let another goroutine waiting on the mutex to grab + // the mutex before it is written to disk. + a.dirty = true + a.mtx.Unlock() + if err := a.writeDirtyToDisk(); err != nil { + log.Errorf("cannot write dirty wallet: %v", err) + } + + log.Infof("Imported payment address %v", addr) + + // Return the payment address string of the imported private key. + return addr, nil +} + // Track requests btcd to send notifications of new transactions for // each address stored in a wallet and sets up a new reply handler for // these notifications. @@ -158,18 +247,18 @@ func (a *Account) Track() { a.UtxoStore.RUnlock() } -// RescanToBestBlock requests btcd to rescan the blockchain for new -// transactions to all wallet addresses. This is needed for making -// btcwallet catch up to a long-running btcd process, as otherwise +// RescanActiveAddresse requests btcd to rescan the blockchain for new +// transactions to all active wallet addresses. This is needed for +// catching btcwallet up to a long-running btcd process, as otherwise // it would have missed notifications as blocks are attached to the // main chain. -func (a *Account) RescanToBestBlock() { +func (a *Account) RescanActiveAddresses() { + // Determine the block to begin the rescan from. beginBlock := int32(0) - if a.fullRescan { // Need to perform a complete rescan since the wallet creation // block. - beginBlock = a.CreatedAt() + beginBlock = a.EarliestBlockHeight() log.Debugf("Rescanning account '%v' for new transactions since block height %v", a.name, beginBlock) } else { @@ -184,9 +273,17 @@ func (a *Account) RescanToBestBlock() { beginBlock = bs.Height + 1 } + // Rescan active addresses starting at the determined block height. + a.RescanAddresses(beginBlock, a.ActivePaymentAddresses()) +} + +// RescanAddresses requests btcd to rescan a set of addresses. This +// is needed when, for example, importing private key(s), where btcwallet +// is synced with btcd for all but several address. +func (a *Account) RescanAddresses(beginBlock int32, addrs map[string]struct{}) { n := <-NewJSONID cmd, err := btcws.NewRescanCmd(fmt.Sprintf("btcwallet(%v)", n), - beginBlock, a.ActivePaymentAddresses()) + beginBlock, addrs) if err != nil { log.Errorf("cannot create rescan request: %v", err) return diff --git a/cmdmgr.go b/cmdmgr.go index d6e841e..37638ed 100644 --- a/cmdmgr.go +++ b/cmdmgr.go @@ -39,9 +39,12 @@ type cmdHandler func(chan []byte, btcjson.Cmd) var rpcHandlers = map[string]cmdHandler{ // Standard bitcoind methods + "dumpprivkey": DumpPrivKey, + "dumpwallet": DumpWallet, "getaddressesbyaccount": GetAddressesByAccount, "getbalance": GetBalance, "getnewaddress": GetNewAddress, + "importprivkey": ImportPrivKey, "listaccounts": ListAccounts, "sendfrom": SendFrom, "sendmany": SendMany, @@ -59,10 +62,11 @@ var wsHandlers = map[string]cmdHandler{ "walletislocked": WalletIsLocked, } -// ProcessFrontendMsg checks the message sent from a frontend. If the -// message method is one that must be handled by btcwallet, the request -// is processed here. Otherwise, the message is sent to btcd. -func ProcessFrontendMsg(frontend chan []byte, msg []byte, ws bool) { +// ProcessRequest checks the requests sent from a frontend. If the +// request method is one that must be handled by btcwallet, the +// request is processed here. Otherwise, the request is sent to btcd +// and btcd's reply is routed back to the frontend. +func ProcessRequest(frontend chan []byte, msg []byte, ws bool) { // Parse marshaled command and check cmd, err := btcjson.ParseMarshaledCmd(msg) if err != nil { @@ -131,7 +135,7 @@ func RouteID(origID, routeID interface{}) string { return fmt.Sprintf("btcwallet(%v)-%v", routeID, origID) } -// ReplyError creates and marshalls a btcjson.Reply with the error e, +// ReplyError creates and marshals a btcjson.Reply with the error e, // sending the reply to a frontend reply channel. func ReplyError(frontend chan []byte, id interface{}, e *btcjson.Error) { // Create a Reply with a non-nil error to marshal. @@ -146,7 +150,7 @@ func ReplyError(frontend chan []byte, id interface{}, e *btcjson.Error) { } } -// ReplySuccess creates and marshalls a btcjson.Reply with the result r, +// ReplySuccess creates and marshals a btcjson.Reply with the result r, // sending the reply to a frontend reply channel. func ReplySuccess(frontend chan []byte, id interface{}, result interface{}) { // Create a Reply with a non-nil result to marshal. @@ -161,6 +165,90 @@ func ReplySuccess(frontend chan []byte, id interface{}, result interface{}) { } } +// DumpPrivKey replies to a dumpprivkey request with the private +// key for a single address, or an appropiate error if the wallet +// is locked. +func DumpPrivKey(frontend chan []byte, icmd btcjson.Cmd) { + // Type assert icmd to access parameters. + cmd, ok := icmd.(*btcjson.DumpPrivKeyCmd) + if !ok { + ReplyError(frontend, icmd.Id(), &btcjson.ErrInternal) + return + } + + // Iterate over all accounts, returning the key if it is found + // in any wallet. + for _, a := range accounts.m { + switch key, err := a.DumpWIFPrivateKey(cmd.Address); err { + case wallet.ErrAddressNotFound: + // Move on to the next account. + continue + + case wallet.ErrWalletLocked: + // Address was found, but the private key isn't + // accessible. + ReplyError(frontend, cmd.Id(), &btcjson.ErrWalletUnlockNeeded) + return + + case nil: + // Key was found. + ReplySuccess(frontend, cmd.Id(), key) + return + + default: // all other non-nil errors + e := &btcjson.Error{ + Code: btcjson.ErrWallet.Code, + Message: err.Error(), + } + ReplyError(frontend, cmd.Id(), e) + return + } + } + + // If this is reached, all accounts have been checked, but none + // have they address. + e := &btcjson.Error{ + Code: btcjson.ErrWallet.Code, + Message: "Address does not refer to a key", + } + ReplyError(frontend, cmd.Id(), e) +} + +// DumpWallet replies to a dumpwallet request with all private keys +// in a wallet, or an appropiate error if the wallet is locked. +func DumpWallet(frontend chan []byte, icmd btcjson.Cmd) { + // Type assert icmd to access parameters. + cmd, ok := icmd.(*btcjson.DumpWalletCmd) + if !ok { + ReplyError(frontend, icmd.Id(), &btcjson.ErrInternal) + return + } + + // Iterate over all accounts, appending the private keys + // for each. + var keys []string + for _, a := range accounts.m { + switch walletKeys, err := a.DumpPrivKeys(); err { + case wallet.ErrWalletLocked: + ReplyError(frontend, cmd.Id(), &btcjson.ErrWalletUnlockNeeded) + return + + case nil: + keys = append(keys, walletKeys...) + + default: // any other non-nil error + e := &btcjson.Error{ + Code: btcjson.ErrWallet.Code, + Message: err.Error(), + } + ReplyError(frontend, cmd.Id(), e) + } + } + + // Reply with sorted WIF encoded private keys + ReplySuccess(frontend, cmd.Id(), keys) +} + // GetAddressesByAccount replies to a getaddressesbyaccount request with // all addresses for an account, or an error if the requested account does // not exist. @@ -213,6 +301,59 @@ func GetBalances(frontend chan []byte, cmd btcjson.Cmd) { NotifyBalances(frontend) } +// ImportPrivKey replies to an importprivkey request by parsing +// a WIF-encoded private key and adding it to an account. +func ImportPrivKey(frontend chan []byte, icmd btcjson.Cmd) { + // Type assert icmd to access parameters. + cmd, ok := icmd.(*btcjson.ImportPrivKeyCmd) + if !ok { + ReplyError(frontend, icmd.Id(), &btcjson.ErrInternal) + return + } + + // Check that the account specified in the requests exists. + // Yes, Label is the account name. + a, ok := accounts.m[cmd.Label] + if !ok { + ReplyError(frontend, cmd.Id(), + &btcjson.ErrWalletInvalidAccountName) + return + } + + // Create a blockstamp for when this address first appeared. + // Because the importprivatekey RPC call does not allow + // specifying when the address first appeared, we must make + // a worst case guess. + bs := &wallet.BlockStamp{Height: 0} + + // Attempt importing the private key, replying with an appropiate + // error if the import was unsuccesful. + addr, err := a.ImportWIFPrivateKey(cmd.PrivKey, cmd.Label, bs) + switch { + case err == wallet.ErrWalletLocked: + ReplyError(frontend, cmd.Id(), &btcjson.ErrWalletUnlockNeeded) + return + + case err != nil: + e := &btcjson.Error{ + Code: btcjson.ErrWallet.Code, + Message: err.Error(), + } + ReplyError(frontend, cmd.Id(), e) + return + } + + if cmd.Rescan { + addrs := map[string]struct{}{ + addr: struct{}{}, + } + a.RescanAddresses(bs.Height, addrs) + } + + // If the import was successful, reply with nil. + ReplySuccess(frontend, cmd.Id(), nil) +} + // NotifyBalances notifies an attached frontend of the current confirmed // and unconfirmed account balances. // diff --git a/sockets.go b/sockets.go index a41362f..a27a189 100644 --- a/sockets.go +++ b/sockets.go @@ -142,7 +142,7 @@ func (s *server) handleRPCRequest(w http.ResponseWriter, r *http.Request) { close(done) }() - ProcessFrontendMsg(frontend, body, false) + ProcessRequest(frontend, body, false) <-done } @@ -258,7 +258,7 @@ func frontendSendRecv(ws *websocket.Conn) { return } // Handle request here. - go ProcessFrontendMsg(frontendNotification, m, true) + go ProcessRequest(frontendNotification, m, true) case ntfn, _ := <-frontendNotification: if err := websocket.Message.Send(ws, ntfn); err != nil { // Frontend disconnected. @@ -703,7 +703,7 @@ func BtcdHandshake(ws *websocket.Conn) { // catch up. for _, a := range accounts.m { - a.RescanToBestBlock() + a.RescanActiveAddresses() } // Begin tracking wallets against this btcd instance. diff --git a/wallet/wallet.go b/wallet/wallet.go index c0adc87..412b0c1 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -248,7 +248,7 @@ func ChainedPrivKey(privkey, pubkey, chaincode []byte) ([]byte, error) { type varEntries []io.WriterTo func (v *varEntries) WriteTo(w io.Writer) (n int64, err error) { - ss := ([]io.WriterTo)(*v) + ss := []io.WriterTo(*v) var written int64 for _, s := range ss { @@ -266,7 +266,7 @@ func (v *varEntries) ReadFrom(r io.Reader) (n int64, err error) { // Remove any previous entries. *v = nil - wts := ([]io.WriterTo)(*v) + wts := []io.WriterTo(*v) // Keep reading entries until an EOF is reached. for { @@ -326,7 +326,6 @@ type comment []byte // Wallet represents an btcd/Armory wallet in memory. It // implements the io.ReaderFrom and io.WriterTo interfaces to read // from and write to any type of byte streams, including files. -// TODO(jrick) remove as many more magic numbers as possible. type Wallet struct { version uint32 net btcwire.BitcoinNet @@ -353,8 +352,9 @@ type Wallet struct { sync.Mutex key []byte } - chainIdxMap map[int64]addressHashKey - lastChainIdx int64 + chainIdxMap map[int64]addressHashKey + importedAddrs []*btcAddress + lastChainIdx int64 } // UnusedWalletBytes specifies the number of actually unused bytes @@ -372,7 +372,9 @@ const UnusedWalletBytes = 1024 - 4 - btcwire.HashSize // desc's binary representation must not exceed 32 and 256 bytes, // respectively. All address private keys are encrypted with passphrase. // The wallet is returned unlocked. -func NewWallet(name, desc string, passphrase []byte, net btcwire.BitcoinNet, createdAt *BlockStamp) (*Wallet, error) { +func NewWallet(name, desc string, passphrase []byte, net btcwire.BitcoinNet, + createdAt *BlockStamp) (*Wallet, error) { + // Check sizes of inputs. if len([]byte(name)) > 32 { return nil, errors.New("name exceeds 32 byte maximum size") @@ -420,7 +422,7 @@ func NewWallet(name, desc string, passphrase []byte, net btcwire.BitcoinNet, cre watchingOnly: false, }, createDate: time.Now().Unix(), - highestUsed: -1, + highestUsed: rootKeyChainIdx, kdfParams: *kdfp, keyGenerator: *root, syncedBlockHeight: createdAt.Height, @@ -436,7 +438,7 @@ func NewWallet(name, desc string, passphrase []byte, net btcwire.BitcoinNet, cre // Add root address to maps. w.addrMap[addressHashKey(w.keyGenerator.pubKeyHash[:])] = &w.keyGenerator - w.chainIdxMap[w.keyGenerator.chainIndex] = addressHashKey(w.keyGenerator.pubKeyHash[:]) + w.chainIdxMap[rootKeyChainIdx] = addressHashKey(w.keyGenerator.pubKeyHash[:]) // Pre-generate encrypted addresses and add to maps. addr := &w.keyGenerator @@ -449,16 +451,17 @@ func NewWallet(name, desc string, passphrase []byte, net btcwire.BitcoinNet, cre if err != nil { return nil, err } - newaddr, err := newBtcAddress(privkey, nil, createdAt) + newaddr, err := newBtcAddress(privkey, nil, createdAt, true) if err != nil { return nil, err } if err = newaddr.encrypt(aeskey); err != nil { return nil, err } - w.addrMap[addressHashKey(newaddr.pubKeyHash[:])] = newaddr + addrKey := addressHashKey(newaddr.pubKeyHash[:]) + w.addrMap[addrKey] = newaddr newaddr.chainIndex = addr.chainIndex + 1 - w.chainIdxMap[newaddr.chainIndex] = addressHashKey(newaddr.pubKeyHash[:]) + w.chainIdxMap[newaddr.chainIndex] = addrKey copy(newaddr.chaincode[:], cc) // armory does this.. but why? addr = newaddr } @@ -523,27 +526,35 @@ func (w *Wallet) ReadFrom(r io.Reader) (n int64, err error) { return n, errors.New("unknown file ID") } - // Add root address to address map - w.addrMap[addressHashKey(w.keyGenerator.pubKeyHash[:])] = &w.keyGenerator - w.chainIdxMap[w.keyGenerator.chainIndex] = addressHashKey(w.keyGenerator.pubKeyHash[:]) + // Add root address to address map. + rootAddrKey := addressHashKey(w.keyGenerator.pubKeyHash[:]) + w.addrMap[rootAddrKey] = &w.keyGenerator + w.chainIdxMap[rootKeyChainIdx] = rootAddrKey // Fill unserializied fields. wts := ([]io.WriterTo)(appendedEntries) for _, wt := range wts { - switch wt.(type) { + switch e := wt.(type) { case *addrEntry: - e := wt.(*addrEntry) - w.addrMap[addressHashKey(e.pubKeyHash160[:])] = &e.addr - w.chainIdxMap[e.addr.chainIndex] = addressHashKey(e.pubKeyHash160[:]) - if w.lastChainIdx < e.addr.chainIndex { - w.lastChainIdx = e.addr.chainIndex + addrKey := addressHashKey(e.pubKeyHash160[:]) + w.addrMap[addrKey] = &e.addr + if e.addr.chainIndex == importedKeyChainIdx { + w.importedAddrs = append(w.importedAddrs, &e.addr) + } else { + w.chainIdxMap[e.addr.chainIndex] = addrKey + if w.lastChainIdx < e.addr.chainIndex { + w.lastChainIdx = e.addr.chainIndex + } } + case *addrCommentEntry: - e := wt.(*addrCommentEntry) - w.addrCommentMap[addressHashKey(e.pubKeyHash160[:])] = comment(e.comment) + addrKey := addressHashKey(e.pubKeyHash160[:]) + w.addrCommentMap[addrKey] = comment(e.comment) + case *txCommentEntry: - e := wt.(*txCommentEntry) - w.txCommentMap[transactionHashKey(e.txHash[:])] = comment(e.comment) + txKey := transactionHashKey(e.txHash[:]) + w.txCommentMap[txKey] = comment(e.comment) + default: return n, errors.New("unknown appended entry") } @@ -555,16 +566,24 @@ func (w *Wallet) ReadFrom(r io.Reader) (n int64, err error) { // WriteTo serializes a Wallet and writes it to a io.Writer, // returning the number of bytes written and any errors encountered. func (w *Wallet) WriteTo(wtr io.Writer) (n int64, err error) { - wts := make([]io.WriterTo, len(w.addrMap)-1) + var wts []io.WriterTo + var chainedAddrs = make([]io.WriterTo, len(w.chainIdxMap)-1) + var importedAddrs []io.WriterTo for hash, addr := range w.addrMap { - if addr.chainIndex != -1 { // ignore root address - e := addrEntry{ - addr: *addr, - } - copy(e.pubKeyHash160[:], []byte(hash)) - wts[addr.chainIndex] = &e + e := &addrEntry{ + addr: *addr, + } + copy(e.pubKeyHash160[:], []byte(hash)) + if addr.chainIndex >= 0 { + // Chained addresses are sorted. This is + // kind of nice but probably isn't necessary. + chainedAddrs[addr.chainIndex] = e + } else if addr.chainIndex == importedKeyChainIdx { + // No order for imported addresses. + importedAddrs = append(importedAddrs, e) } } + wts = append(chainedAddrs, importedAddrs...) for hash, comment := range w.addrCommentMap { e := &addrCommentEntry{ comment: []byte(comment), @@ -689,8 +708,8 @@ func (w *Wallet) Version() (string, int) { // and will return an empty string if the address pool has run out. func (w *Wallet) NextUnusedAddress() (string, error) { // Attempt to get address hash of next chained address. - next160, err := w.addr160ForIdx(w.highestUsed + 1) - if err != nil { + next160, ok := w.chainIdxMap[w.highestUsed+1] + if !ok { // TODO(jrick): Re-fill key pool. return "", errors.New("cannot find generated address") } else { @@ -823,18 +842,75 @@ func (w *Wallet) SyncedWith() *BlockStamp { } } -// CreatedAt returns the height of the blockchain at the time of wallet -// creation. This is needed when performaing a full rescan to prevent -// unnecessary rescanning before wallet addresses first appeared. -func (w *Wallet) CreatedAt() int32 { - return w.keyGenerator.firstBlock +// EarliestBlockHeight returns the height of the blockchain for when any +// wallet address first appeared. This will usually be the block height +// at the time of wallet creation, unless a private key with an earlier +// block height was imported into the wallet. This is needed when +// performing a full rescan to prevent unnecessary rescanning before +// wallet addresses first appeared. +func (w *Wallet) EarliestBlockHeight() int32 { + height := w.keyGenerator.firstBlock + + // Imported keys will be the only ones that may have an earlier + // blockchain height. Check each and set the returned height + for _, addr := range w.importedAddrs { + if addr.firstBlock < height { + height = addr.firstBlock + + // Can't go any lower than 0. + if height == 0 { + break + } + } + } + + return height } -func (w *Wallet) addr160ForIdx(idx int64) (addressHashKey, error) { - if idx > w.lastChainIdx { - return "", errors.New("chain index out of range") +// ImportPrivateKey creates a new encrypted btcAddress with a +// user-provided private key and adds it to the wallet. If the +// import is successful, the payment address string is returned. +func (w *Wallet) ImportPrivateKey(privkey []byte, compressed bool, + bs *BlockStamp) (string, error) { + + // The wallet's secret will be zeroed on lock, so make a local + // copy. + w.secret.Lock() + if len(w.secret.key) != 32 { + w.secret.Unlock() + return "", ErrWalletLocked } - return w.chainIdxMap[idx], nil + localSecret := make([]byte, 32) + copy(localSecret, w.secret.key) + w.secret.Unlock() + + // Create new address with this private key. + addr, err := newBtcAddress(privkey, nil, bs, compressed) + if err != nil { + return "", err + } + addr.chainIndex = importedKeyChainIdx + + // Encrypt imported address with the derived AES key. + if err = addr.encrypt(localSecret); err != nil { + return "", err + } + + // Create payment address string. If this fails, return an error + // before adding the address to the wallet. + addr160 := addr.pubKeyHash[:] + addrstr, err := btcutil.EncodeAddress(addr160, w.Net()) + if err != nil { + return "", err + } + + // Add address to wallet's bookkeeping structures. Adding to + // the map will result in the imported address being serialized + // on the next WriteTo call. + w.addrMap[addressHashKey(addr160)] = addr + w.importedAddrs = append(w.importedAddrs, addr) + + return addrstr, nil } // AddressInfo holds information regarding an address needed to manage @@ -842,8 +918,9 @@ func (w *Wallet) addr160ForIdx(idx int64) (addressHashKey, error) { type AddressInfo struct { Address string AddrHash string - FirstBlock int32 Compressed bool + FirstBlock int32 + Imported bool } // GetSortedActiveAddresses returns all wallet addresses that have been @@ -851,10 +928,11 @@ type AddressInfo struct { // the key pool. Use this when ordered addresses are needed. Otherwise, // GetActiveAddresses is preferred. func (w *Wallet) GetSortedActiveAddresses() []*AddressInfo { - addrs := make([]*AddressInfo, 0, w.highestUsed+1) - for i := int64(-1); i <= w.highestUsed; i++ { - addr160, err := w.addr160ForIdx(i) - if err != nil { + addrs := make([]*AddressInfo, 0, + w.highestUsed+int64(len(w.importedAddrs))+1) + for i := int64(rootKeyChainIdx); i <= w.highestUsed; i++ { + addr160, ok := w.chainIdxMap[i] + if !ok { return addrs } addr := w.addrMap[addr160] @@ -863,6 +941,12 @@ func (w *Wallet) GetSortedActiveAddresses() []*AddressInfo { addrs = append(addrs, info) } } + for _, addr := range w.importedAddrs { + info, err := addr.info(w.Net()) + if err == nil { + addrs = append(addrs, info) + } + } return addrs } @@ -871,9 +955,9 @@ func (w *Wallet) GetSortedActiveAddresses() []*AddressInfo { // key pool. If addresses must be sorted, use GetSortedActiveAddresses. func (w *Wallet) GetActiveAddresses() map[string]*AddressInfo { addrs := make(map[string]*AddressInfo) - for i := int64(-1); i <= w.highestUsed; i++ { - addr160, err := w.addr160ForIdx(i) - if err != nil { + for i := int64(rootKeyChainIdx); i <= w.highestUsed; i++ { + addr160, ok := w.chainIdxMap[i] + if !ok { return addrs } addr := w.addrMap[addr160] @@ -882,6 +966,12 @@ func (w *Wallet) GetActiveAddresses() map[string]*AddressInfo { addrs[info.Address] = info } } + for _, addr := range w.importedAddrs { + info, err := addr.info(w.Net()) + if err == nil { + addrs[info.Address] = info + } + } return addrs } @@ -988,6 +1078,16 @@ type btcAddress struct { } } +const ( + // Root address has a chain index of -1. Each subsequent + // chained address increments the index. + rootKeyChainIdx = -1 + + // Imported private keys are not part of the chain, and have a + // special index of -2. + importedKeyChainIdx = -2 +) + const ( pubkeyCompressed byte = 0x2 pubkeyUncompressed byte = 0x4 @@ -1039,7 +1139,7 @@ func (k *publicKey) WriteTo(w io.Writer) (n int64, err error) { // newBtcAddress initializes and returns a new address. privkey must // be 32 bytes. iv must be 16 bytes, or nil (in which case it is // randomly generated). -func newBtcAddress(privkey, iv []byte, bs *BlockStamp) (addr *btcAddress, err error) { +func newBtcAddress(privkey, iv []byte, bs *BlockStamp, compressed bool) (addr *btcAddress, err error) { if len(privkey) != 32 { return nil, errors.New("private key is not 32 bytes") } @@ -1056,7 +1156,7 @@ func newBtcAddress(privkey, iv []byte, bs *BlockStamp) (addr *btcAddress, err er flags: addrFlags{ hasPrivKey: true, hasPubKey: true, - compressed: true, + compressed: compressed, }, firstSeen: time.Now().Unix(), firstBlock: bs.Height, @@ -1072,18 +1172,22 @@ func newBtcAddress(privkey, iv []byte, bs *BlockStamp) (addr *btcAddress, err er // newRootBtcAddress generates a new address, also setting the // chaincode and chain index to represent this address as a root // address. -func newRootBtcAddress(privKey, iv, chaincode []byte, bs *BlockStamp) (addr *btcAddress, err error) { +func newRootBtcAddress(privKey, iv, chaincode []byte, + bs *BlockStamp) (addr *btcAddress, err error) { + if len(chaincode) != 32 { return nil, errors.New("chaincode is not 32 bytes") } - addr, err = newBtcAddress(privKey, iv, bs) + // Create new btcAddress with provided inputs. This will + // always use a compressed pubkey. + addr, err = newBtcAddress(privKey, iv, bs, true) if err != nil { return nil, err } copy(addr.chaincode[:], chaincode) - addr.chainIndex = -1 + addr.chainIndex = rootKeyChainIdx return addr, err } @@ -1299,8 +1403,9 @@ func (a *btcAddress) info(net btcwire.BitcoinNet) (*AddressInfo, error) { return &AddressInfo{ Address: address, AddrHash: string(a.pubKeyHash[:]), - FirstBlock: a.firstBlock, Compressed: a.flags.compressed, + FirstBlock: a.firstBlock, + Imported: a.chainIndex == importedKeyChainIdx, }, nil }