Skip to content

Commit

Permalink
Add private key import and export support.
Browse files Browse the repository at this point in the history
This adds the necessary bits for handling importing addresses for the
wallet file format, as well as implementing the importprivkey and
dumpprivkey RPC requests.

Initial code by dhill.
  • Loading branch information
jrick committed Nov 20, 2013
1 parent 0bd8772 commit 00fe439
Show file tree
Hide file tree
Showing 4 changed files with 417 additions and 74 deletions.
115 changes: 106 additions & 9 deletions account.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package main

import (
"bytes"
"errors"
"fmt"
"github.com/conformal/btcjson"
"github.com/conformal/btcutil"
Expand Down Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down
153 changes: 147 additions & 6 deletions cmdmgr.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 {
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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.
//
Expand Down
6 changes: 3 additions & 3 deletions sockets.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit 00fe439

Please sign in to comment.