Switch to new waddrmgr package

This commit converts the wallet to use the new secure hierarchical
deterministic wallet address manager package as well as the walletdb
package.

The following is an overview of modified functionality:

- The wallet must now be created before starting the executable
- A new flag --create has been added to create the new wallet using wizard
  style question and answer prompts
- Starting the process without an existing wallet will instruct now
  display a message to run it with --create
- Providing the --create flag with an existing wallet will simply show an
  error and return

In addition the snacl package has been modified to return the memory after
performing scrypt operations to the OS.

Previously a runtime.GC was being invoked which forced it to release the
memory as far as the garbage collector is concerned, but the memory was
not released back to the OS immediatley.  This modification allows the
memory to be released immedately since it won't be needed again until the
next wallet unlock.
This commit is contained in:
Dave Collins 2014-10-29 01:43:29 -05:00
parent 130e44c761
commit 8f9f53a618
14 changed files with 915 additions and 601 deletions

View file

@ -68,6 +68,15 @@ func walletMain() error {
}() }()
} }
// Load the wallet database. It must have been created with the
// --create option already or this will return an appropriate error.
wallet, err := openWallet()
if err != nil {
log.Errorf("%v", err)
return err
}
defer wallet.db.Close()
// Create and start HTTP server to serve wallet client connections. // Create and start HTTP server to serve wallet client connections.
// This will be updated with the wallet and chain server RPC client // This will be updated with the wallet and chain server RPC client
// created below after each is created. // created below after each is created.
@ -78,6 +87,7 @@ func walletMain() error {
return err return err
} }
server.Start() server.Start()
server.SetWallet(wallet)
// Shutdown the server if an interrupt signal is received. // Shutdown the server if an interrupt signal is received.
addInterruptHandler(server.Stop) addInterruptHandler(server.Stop)
@ -121,49 +131,16 @@ func walletMain() error {
chainSvrChan <- rpcc chainSvrChan <- rpcc
}() }()
// Create a channel to report unrecoverable errors during the loading of
// the wallet files. These may include OS file handling errors or
// issues deserializing the wallet files, but does not include missing
// wallet files (as that must be handled by creating a new wallet).
walletOpenErrors := make(chan error)
go func() { go func() {
defer close(walletOpenErrors)
// Open wallet structures from disk.
w, err := openWallet()
if err != nil {
if os.IsNotExist(err) {
// If the keystore file is missing, notify the server
// that generating new wallets is ok.
server.SetWallet(nil)
return
}
// If the keystore file exists but another error was
// encountered, we cannot continue.
log.Errorf("Cannot load wallet files: %v", err)
walletOpenErrors <- err
return
}
server.SetWallet(w)
// Start wallet goroutines and handle RPC client notifications // Start wallet goroutines and handle RPC client notifications
// if the chain server connection was opened. // if the chain server connection was opened.
select { select {
case chainSvr := <-chainSvrChan: case chainSvr := <-chainSvrChan:
w.Start(chainSvr) wallet.Start(chainSvr)
case <-server.quit: case <-server.quit:
} }
}() }()
// Check for unrecoverable errors during the wallet startup, and return
// the error, if any.
err, ok := <-walletOpenErrors
if ok {
return err
}
// Wait for the server to shutdown either due to a stop RPC request // Wait for the server to shutdown either due to a stop RPC request
// or an interrupt. // or an interrupt.
server.WaitForShutdown() server.WaitForShutdown()

View file

@ -26,8 +26,8 @@ import (
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcrpcclient" "github.com/btcsuite/btcrpcclient"
"github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil"
"github.com/btcsuite/btcwallet/legacy/keystore"
"github.com/btcsuite/btcwallet/txstore" "github.com/btcsuite/btcwallet/txstore"
"github.com/btcsuite/btcwallet/waddrmgr"
) )
// Client represents a persistent client connection to a bitcoin RPC server // Client represents a persistent client connection to a bitcoin RPC server
@ -38,7 +38,7 @@ type Client struct {
enqueueNotification chan interface{} enqueueNotification chan interface{}
dequeueNotification chan interface{} dequeueNotification chan interface{}
currentBlock chan *keystore.BlockStamp currentBlock chan *waddrmgr.BlockStamp
// Notification channels regarding the state of the client. These exist // Notification channels regarding the state of the client. These exist
// so other components can listen in on chain activity. These are // so other components can listen in on chain activity. These are
@ -64,7 +64,7 @@ func NewClient(chainParams *chaincfg.Params, connect, user, pass string, certs [
chainParams: chainParams, chainParams: chainParams,
enqueueNotification: make(chan interface{}), enqueueNotification: make(chan interface{}),
dequeueNotification: make(chan interface{}), dequeueNotification: make(chan interface{}),
currentBlock: make(chan *keystore.BlockStamp), currentBlock: make(chan *waddrmgr.BlockStamp),
notificationLock: new(sync.Mutex), notificationLock: new(sync.Mutex),
quit: make(chan struct{}), quit: make(chan struct{}),
} }
@ -157,11 +157,11 @@ func (c *Client) WaitForShutdown() {
type ( type (
// BlockConnected is a notification for a newly-attached block to the // BlockConnected is a notification for a newly-attached block to the
// best chain. // best chain.
BlockConnected keystore.BlockStamp BlockConnected waddrmgr.BlockStamp
// BlockDisconnected is a notifcation that the block described by the // BlockDisconnected is a notifcation that the block described by the
// BlockStamp was reorganized out of the best chain. // BlockStamp was reorganized out of the best chain.
BlockDisconnected keystore.BlockStamp BlockDisconnected waddrmgr.BlockStamp
// RecvTx is a notification for a transaction which pays to a wallet // RecvTx is a notification for a transaction which pays to a wallet
// address. // address.
@ -204,7 +204,7 @@ func (c *Client) Notifications() <-chan interface{} {
// BlockStamp returns the latest block notified by the client, or an error // BlockStamp returns the latest block notified by the client, or an error
// if the client has been shut down. // if the client has been shut down.
func (c *Client) BlockStamp() (*keystore.BlockStamp, error) { func (c *Client) BlockStamp() (*waddrmgr.BlockStamp, error) {
select { select {
case bs := <-c.currentBlock: case bs := <-c.currentBlock:
return bs, nil return bs, nil
@ -238,11 +238,11 @@ func (c *Client) onClientConnect() {
} }
func (c *Client) onBlockConnected(hash *wire.ShaHash, height int32) { func (c *Client) onBlockConnected(hash *wire.ShaHash, height int32) {
c.enqueueNotification <- BlockConnected{Hash: hash, Height: height} c.enqueueNotification <- BlockConnected{Hash: *hash, Height: height}
} }
func (c *Client) onBlockDisconnected(hash *wire.ShaHash, height int32) { func (c *Client) onBlockDisconnected(hash *wire.ShaHash, height int32) {
c.enqueueNotification <- BlockDisconnected{Hash: hash, Height: height} c.enqueueNotification <- BlockDisconnected{Hash: *hash, Height: height}
} }
func (c *Client) onRecvTx(tx *btcutil.Tx, block *btcws.BlockDetails) { func (c *Client) onRecvTx(tx *btcutil.Tx, block *btcws.BlockDetails) {
@ -294,7 +294,7 @@ func (c *Client) handler() {
c.wg.Done() c.wg.Done()
} }
bs := &keystore.BlockStamp{Hash: hash, Height: height} bs := &waddrmgr.BlockStamp{Hash: *hash, Height: height}
// TODO: Rather than leaving this as an unbounded queue for all types of // TODO: Rather than leaving this as an unbounded queue for all types of
// notifications, try dropping ones where a later enqueued notification // notifications, try dropping ones where a later enqueued notification
@ -329,7 +329,7 @@ out:
case dequeue <- next: case dequeue <- next:
if n, ok := next.(BlockConnected); ok { if n, ok := next.(BlockConnected); ok {
bs = (*keystore.BlockStamp)(&n) bs = (*waddrmgr.BlockStamp)(&n)
} }
notifications[0] = nil notifications[0] = nil

View file

@ -20,8 +20,8 @@ import (
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil"
"github.com/btcsuite/btcwallet/chain" "github.com/btcsuite/btcwallet/chain"
"github.com/btcsuite/btcwallet/legacy/keystore"
"github.com/btcsuite/btcwallet/txstore" "github.com/btcsuite/btcwallet/txstore"
"github.com/btcsuite/btcwallet/waddrmgr"
) )
func (w *Wallet) handleChainNotifications() { func (w *Wallet) handleChainNotifications() {
@ -29,9 +29,9 @@ func (w *Wallet) handleChainNotifications() {
var err error var err error
switch n := n.(type) { switch n := n.(type) {
case chain.BlockConnected: case chain.BlockConnected:
w.connectBlock(keystore.BlockStamp(n)) w.connectBlock(waddrmgr.BlockStamp(n))
case chain.BlockDisconnected: case chain.BlockDisconnected:
w.disconnectBlock(keystore.BlockStamp(n)) w.disconnectBlock(waddrmgr.BlockStamp(n))
case chain.RecvTx: case chain.RecvTx:
err = w.addReceivedTx(n.Tx, n.Block) err = w.addReceivedTx(n.Tx, n.Block)
case chain.RedeemingTx: case chain.RedeemingTx:
@ -53,13 +53,16 @@ func (w *Wallet) handleChainNotifications() {
// connectBlock handles a chain server notification by marking a wallet // connectBlock handles a chain server notification by marking a wallet
// that's currently in-sync with the chain server as being synced up to // that's currently in-sync with the chain server as being synced up to
// the passed block. // the passed block.
func (w *Wallet) connectBlock(bs keystore.BlockStamp) { func (w *Wallet) connectBlock(bs waddrmgr.BlockStamp) {
if !w.ChainSynced() { if !w.ChainSynced() {
return return
} }
w.KeyStore.SetSyncedWith(&bs) if err := w.Manager.SetSyncedTo(&bs); err != nil {
w.KeyStore.MarkDirty() log.Errorf("failed to update address manager sync state in "+
"connect block for hash %v (height %d): %v", bs.Hash,
bs.Height, err)
}
w.notifyConnectedBlock(bs) w.notifyConnectedBlock(bs)
w.notifyBalances(bs.Height) w.notifyBalances(bs.Height)
@ -68,22 +71,26 @@ func (w *Wallet) connectBlock(bs keystore.BlockStamp) {
// disconnectBlock handles a chain server reorganize by rolling back all // disconnectBlock handles a chain server reorganize by rolling back all
// block history from the reorged block for a wallet in-sync with the chain // block history from the reorged block for a wallet in-sync with the chain
// server. // server.
func (w *Wallet) disconnectBlock(bs keystore.BlockStamp) { func (w *Wallet) disconnectBlock(bs waddrmgr.BlockStamp) {
if !w.ChainSynced() { if !w.ChainSynced() {
return return
} }
// Disconnect the last seen block from the keystore if it // Disconnect the last seen block from the manager if it matches the
// matches the removed block. // removed block.
iter := w.KeyStore.NewIterateRecentBlocks() iter := w.Manager.NewIterateRecentBlocks()
if iter != nil && *iter.BlockStamp().Hash == *bs.Hash { if iter != nil && iter.BlockStamp().Hash == bs.Hash {
if iter.Prev() { if iter.Prev() {
prev := iter.BlockStamp() prev := iter.BlockStamp()
w.KeyStore.SetSyncedWith(&prev) w.Manager.SetSyncedTo(&prev)
} else { } else {
w.KeyStore.SetSyncedWith(nil) // The reorg is farther back than the recently-seen list
// of blocks has recorded, so set it to unsynced which
// will in turn lead to a rescan from either the
// earliest blockstamp the addresses in the manager are
// known to have been created.
w.Manager.SetSyncedTo(nil)
} }
w.KeyStore.MarkDirty()
} }
w.notifyDisconnectedBlock(bs) w.notifyDisconnectedBlock(bs)
@ -103,7 +110,7 @@ func (w *Wallet) addReceivedTx(tx *btcutil.Tx, block *txstore.Block) error {
activeNet.Params) activeNet.Params)
insert := false insert := false
for _, addr := range addrs { for _, addr := range addrs {
_, err := w.KeyStore.Address(addr) _, err := w.Manager.Address(addr)
if err == nil { if err == nil {
insert = true insert = true
break break
@ -150,7 +157,6 @@ func (w *Wallet) addRedeemingTx(tx *btcutil.Tx, block *txstore.Block) error {
if _, err := txr.AddDebits(); err != nil { if _, err := txr.AddDebits(); err != nil {
return err return err
} }
w.KeyStore.MarkDirty()
bs, err := w.chainSvr.BlockStamp() bs, err := w.chainSvr.BlockStamp()
if err == nil { if err == nil {

View file

@ -26,6 +26,7 @@ import (
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil"
"github.com/btcsuite/btcwallet/legacy/keystore"
flags "github.com/btcsuite/go-flags" flags "github.com/btcsuite/go-flags"
) )
@ -39,6 +40,8 @@ const (
defaultDisallowFree = false defaultDisallowFree = false
defaultRPCMaxClients = 10 defaultRPCMaxClients = 10
defaultRPCMaxWebsockets = 25 defaultRPCMaxWebsockets = 25
walletDbName = "wallet.db"
walletDbWatchingOnlyName = "wowallet.db"
) )
var ( var (
@ -54,6 +57,7 @@ var (
type config struct { type config struct {
ShowVersion bool `short:"V" long:"version" description:"Display version information and exit"` ShowVersion bool `short:"V" long:"version" description:"Display version information and exit"`
Create bool `long:"create" description:"Create the wallet if it does not exist"`
CAFile string `long:"cafile" description:"File containing root certificates to authenticate a TLS connections with btcd"` CAFile string `long:"cafile" description:"File containing root certificates to authenticate a TLS connections with btcd"`
RPCConnect string `short:"c" long:"rpcconnect" description:"Hostname/IP and port of btcd RPC server to connect to (default localhost:18334, mainnet: localhost:8334, simnet: localhost:18556)"` RPCConnect string `short:"c" long:"rpcconnect" description:"Hostname/IP and port of btcd RPC server to connect to (default localhost:18334, mainnet: localhost:8334, simnet: localhost:18556)"`
DebugLevel string `short:"d" long:"debuglevel" description:"Logging level {trace, debug, info, warn, error, critical}"` DebugLevel string `short:"d" long:"debuglevel" description:"Logging level {trace, debug, info, warn, error, critical}"`
@ -65,6 +69,7 @@ type config struct {
Password string `short:"P" long:"password" default-mask:"-" description:"Password for client and btcd authorization"` Password string `short:"P" long:"password" default-mask:"-" description:"Password for client and btcd authorization"`
BtcdUsername string `long:"btcdusername" description:"Alternative username for btcd authorization"` BtcdUsername string `long:"btcdusername" description:"Alternative username for btcd authorization"`
BtcdPassword string `long:"btcdpassword" default-mask:"-" description:"Alternative password for btcd authorization"` BtcdPassword string `long:"btcdpassword" default-mask:"-" description:"Alternative password for btcd authorization"`
WalletPass string `long:"walletpass" default-mask:"-" description:"The public wallet password -- Only required if the wallet was created with one"`
RPCCert string `long:"rpccert" description:"File containing the certificate file"` RPCCert string `long:"rpccert" description:"File containing the certificate file"`
RPCKey string `long:"rpckey" description:"File containing the certificate key"` RPCKey string `long:"rpckey" description:"File containing the certificate key"`
RPCMaxClients int64 `long:"rpcmaxclients" description:"Max number of RPC clients for standard connections"` RPCMaxClients int64 `long:"rpcmaxclients" description:"Max number of RPC clients for standard connections"`
@ -242,6 +247,7 @@ func loadConfig() (*config, []string, error) {
ConfigFile: defaultConfigFile, ConfigFile: defaultConfigFile,
DataDir: defaultDataDir, DataDir: defaultDataDir,
LogDir: defaultLogDir, LogDir: defaultLogDir,
WalletPass: defaultPubPassphrase,
RPCKey: defaultRPCKeyFile, RPCKey: defaultRPCKeyFile,
RPCCert: defaultRPCCertFile, RPCCert: defaultRPCCertFile,
DisallowFree: defaultDisallowFree, DisallowFree: defaultDisallowFree,
@ -360,6 +366,47 @@ func loadConfig() (*config, []string, error) {
return nil, nil, err return nil, nil, err
} }
// Ensure the wallet exists or create it when the create flag is set.
netDir := networkDir(cfg.DataDir, activeNet.Params)
dbPath := filepath.Join(netDir, walletDbName)
if cfg.Create {
// Error if the create flag is set and the wallet already
// exists.
if fileExists(dbPath) {
err := fmt.Errorf("The wallet already exists.")
fmt.Fprintln(os.Stderr, err)
return nil, nil, err
}
// Ensure the data directory for the network exists.
if err := checkCreateDir(netDir); err != nil {
fmt.Fprintln(os.Stderr, err)
return nil, nil, err
}
// Perform the initial wallet creation wizard.
if err := createWallet(&cfg); err != nil {
fmt.Fprintln(os.Stderr, "Unable to create wallet:", err)
return nil, nil, err
}
// Created successfully, so exit now with success.
os.Exit(0)
} else if !fileExists(dbPath) {
var err error
keystorePath := filepath.Join(netDir, keystore.Filename)
if !fileExists(keystorePath) {
err = fmt.Errorf("The wallet does not exist. Run with the " +
"--create option to initialize and create it.")
} else {
err = fmt.Errorf("The wallet is in legacy format. Run with the " +
"--create option to import it.")
}
fmt.Fprintln(os.Stderr, err)
return nil, nil, err
}
if cfg.RPCConnect == "" { if cfg.RPCConnect == "" {
cfg.RPCConnect = activeNet.connect cfg.RPCConnect = activeNet.connect
} }

View file

@ -27,8 +27,8 @@ import (
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil"
"github.com/btcsuite/btcwallet/legacy/keystore"
"github.com/btcsuite/btcwallet/txstore" "github.com/btcsuite/btcwallet/txstore"
"github.com/btcsuite/btcwallet/waddrmgr"
) )
const ( const (
@ -130,9 +130,9 @@ func (u ByAmount) Swap(i, j int) { u[i], u[j] = u[j], u[i] }
// eligible unspent outputs to create the transaction. // eligible unspent outputs to create the transaction.
func (w *Wallet) txToPairs(pairs map[string]btcutil.Amount, minconf int) (*CreatedTx, error) { func (w *Wallet) txToPairs(pairs map[string]btcutil.Amount, minconf int) (*CreatedTx, error) {
// Key store must be unlocked to compose transaction. Grab the // Address manager must be unlocked to compose transaction. Grab
// unlock if possible (to prevent future unlocks), or return the // the unlock if possible (to prevent future unlocks), or return the
// error if the keystore is already locked. // error if already locked.
heldUnlock, err := w.HoldUnlock() heldUnlock, err := w.HoldUnlock()
if err != nil { if err != nil {
return nil, err return nil, err
@ -150,7 +150,7 @@ func (w *Wallet) txToPairs(pairs map[string]btcutil.Amount, minconf int) (*Creat
return nil, err return nil, err
} }
return createTx(eligible, pairs, bs, w.FeeIncrement, w.KeyStore, w.changeAddress) return createTx(eligible, pairs, bs, w.FeeIncrement, w.Manager, w.changeAddress)
} }
// createTx selects inputs (from the given slice of eligible utxos) // createTx selects inputs (from the given slice of eligible utxos)
@ -161,10 +161,10 @@ func (w *Wallet) txToPairs(pairs map[string]btcutil.Amount, minconf int) (*Creat
func createTx( func createTx(
eligible []txstore.Credit, eligible []txstore.Credit,
outputs map[string]btcutil.Amount, outputs map[string]btcutil.Amount,
bs *keystore.BlockStamp, bs *waddrmgr.BlockStamp,
feeIncrement btcutil.Amount, feeIncrement btcutil.Amount,
keys *keystore.Store, mgr *waddrmgr.Manager,
changeAddress func(*keystore.BlockStamp) (btcutil.Address, error)) ( changeAddress func(*waddrmgr.BlockStamp) (btcutil.Address, error)) (
*CreatedTx, error) { *CreatedTx, error) {
msgtx := wire.NewMsgTx() msgtx := wire.NewMsgTx()
@ -232,7 +232,7 @@ func createTx(
} }
} }
if err = signMsgTx(msgtx, inputs, keys); err != nil { if err = signMsgTx(msgtx, inputs, mgr); err != nil {
return nil, err return nil, err
} }
@ -296,12 +296,12 @@ func addChange(msgtx *wire.MsgTx, change btcutil.Amount, changeAddr btcutil.Addr
// changeAddress obtains a new btcutil.Address to be used as a change // changeAddress obtains a new btcutil.Address to be used as a change
// transaction output. It will also mark the KeyStore as dirty and // transaction output. It will also mark the KeyStore as dirty and
// tells chainSvr to watch that address. // tells chainSvr to watch that address.
func (w *Wallet) changeAddress(bs *keystore.BlockStamp) (btcutil.Address, error) { func (w *Wallet) changeAddress(bs *waddrmgr.BlockStamp) (btcutil.Address, error) {
changeAddr, err := w.KeyStore.ChangeAddress(bs) changeAddrs, err := w.Manager.NextInternalAddresses(0, 1)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get change address: %s", err) return nil, fmt.Errorf("failed to get change address: %s", err)
} }
w.KeyStore.MarkDirty() changeAddr := changeAddrs[0].Address()
err = w.chainSvr.NotifyReceived([]btcutil.Address{changeAddr}) err = w.chainSvr.NotifyReceived([]btcutil.Address{changeAddr})
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot request updates for "+ return nil, fmt.Errorf("cannot request updates for "+
@ -335,7 +335,7 @@ func addOutputs(msgtx *wire.MsgTx, pairs map[string]btcutil.Amount) (btcutil.Amo
return minAmount, nil return minAmount, nil
} }
func (w *Wallet) findEligibleOutputs(minconf int, bs *keystore.BlockStamp) ([]txstore.Credit, error) { func (w *Wallet) findEligibleOutputs(minconf int, bs *waddrmgr.BlockStamp) ([]txstore.Credit, error) {
unspent, err := w.TxStore.UnspentOutputs() unspent, err := w.TxStore.UnspentOutputs()
if err != nil { if err != nil {
return nil, err return nil, err
@ -374,7 +374,7 @@ func (w *Wallet) findEligibleOutputs(minconf int, bs *keystore.BlockStamp) ([]tx
// signMsgTx sets the SignatureScript for every item in msgtx.TxIn. // signMsgTx sets the SignatureScript for every item in msgtx.TxIn.
// It must be called every time a msgtx is changed. // It must be called every time a msgtx is changed.
// Only P2PKH outputs are supported at this point. // Only P2PKH outputs are supported at this point.
func signMsgTx(msgtx *wire.MsgTx, prevOutputs []txstore.Credit, store *keystore.Store) error { func signMsgTx(msgtx *wire.MsgTx, prevOutputs []txstore.Credit, mgr *waddrmgr.Manager) error {
if len(prevOutputs) != len(msgtx.TxIn) { if len(prevOutputs) != len(msgtx.TxIn) {
return fmt.Errorf( return fmt.Errorf(
"Number of prevOutputs (%d) does not match number of tx inputs (%d)", "Number of prevOutputs (%d) does not match number of tx inputs (%d)",
@ -392,19 +392,20 @@ func signMsgTx(msgtx *wire.MsgTx, prevOutputs []txstore.Credit, store *keystore.
return ErrUnsupportedTransactionType return ErrUnsupportedTransactionType
} }
ai, err := store.Address(apkh) ai, err := mgr.Address(apkh)
if err != nil { if err != nil {
return fmt.Errorf("cannot get address info: %v", err) return fmt.Errorf("cannot get address info: %v", err)
} }
pka := ai.(keystore.PubKeyAddress) pka := ai.(waddrmgr.ManagedPubKeyAddress)
privkey, err := pka.PrivKey() privkey, err := pka.PrivKey()
if err != nil { if err != nil {
return fmt.Errorf("cannot get private key: %v", err) return fmt.Errorf("cannot get private key: %v", err)
} }
sigscript, err := txscript.SignatureScript( sigscript, err := txscript.SignatureScript(msgtx, i,
msgtx, i, output.TxOut().PkScript, txscript.SigHashAll, privkey, ai.Compressed()) output.TxOut().PkScript, txscript.SigHashAll, privkey,
ai.Compressed())
if err != nil { if err != nil {
return fmt.Errorf("cannot create sigscript: %s", err) return fmt.Errorf("cannot create sigscript: %s", err)
} }

View file

@ -2,6 +2,8 @@ package main
import ( import (
"encoding/hex" "encoding/hex"
"os"
"path/filepath"
"reflect" "reflect"
"sort" "sort"
"testing" "testing"
@ -9,8 +11,11 @@ import (
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil"
"github.com/btcsuite/btcwallet/keystore" "github.com/btcsuite/btcutil/hdkeychain"
"github.com/btcsuite/btcwallet/txstore" "github.com/btcsuite/btcwallet/txstore"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/walletdb"
_ "github.com/btcsuite/btcwallet/walletdb/bdb"
) )
// This is a tx that transfers funds (0.371 BTC) to addresses of known privKeys. // This is a tx that transfers funds (0.371 BTC) to addresses of known privKeys.
@ -40,6 +45,14 @@ var (
outAddr2 = "12MzCDwodF9G1e7jfwLXfR164RNtx4BRVG" outAddr2 = "12MzCDwodF9G1e7jfwLXfR164RNtx4BRVG"
) )
// fastScrypt are options to passed to the wallet address manager to speed up
// the scrypt derivations.
var fastScrypt = &waddrmgr.Options{
ScryptN: 16,
ScryptR: 8,
ScryptP: 1,
}
func Test_addOutputs(t *testing.T) { func Test_addOutputs(t *testing.T) {
msgtx := wire.NewMsgTx() msgtx := wire.NewMsgTx()
pairs := map[string]btcutil.Amount{outAddr1: 10, outAddr2: 1} pairs := map[string]btcutil.Amount{outAddr1: 10, outAddr2: 1}
@ -58,10 +71,10 @@ func Test_addOutputs(t *testing.T) {
func TestCreateTx(t *testing.T) { func TestCreateTx(t *testing.T) {
cfg = &config{DisallowFree: false} cfg = &config{DisallowFree: false}
bs := &keystore.BlockStamp{Height: 11111} bs := &waddrmgr.BlockStamp{Height: 11111}
keys := newKeyStore(t, txInfo.privKeys, bs) mgr := newManager(t, txInfo.privKeys, bs)
changeAddr, _ := btcutil.DecodeAddress("muqW4gcixv58tVbSKRC5q6CRKy8RmyLgZ5", activeNet.Params) changeAddr, _ := btcutil.DecodeAddress("muqW4gcixv58tVbSKRC5q6CRKy8RmyLgZ5", activeNet.Params)
var tstChangeAddress = func(bs *keystore.BlockStamp) (btcutil.Address, error) { var tstChangeAddress = func(bs *waddrmgr.BlockStamp) (btcutil.Address, error) {
return changeAddr, nil return changeAddr, nil
} }
@ -69,7 +82,7 @@ func TestCreateTx(t *testing.T) {
eligible := eligibleInputsFromTx(t, txInfo.hex, []uint32{1, 2, 3, 4, 5}) eligible := eligibleInputsFromTx(t, txInfo.hex, []uint32{1, 2, 3, 4, 5})
// Now create a new TX sending 25e6 satoshis to the following addresses: // Now create a new TX sending 25e6 satoshis to the following addresses:
outputs := map[string]btcutil.Amount{outAddr1: 15e6, outAddr2: 10e6} outputs := map[string]btcutil.Amount{outAddr1: 15e6, outAddr2: 10e6}
tx, err := createTx(eligible, outputs, bs, defaultFeeIncrement, keys, tstChangeAddress) tx, err := createTx(eligible, outputs, bs, defaultFeeIncrement, mgr, tstChangeAddress)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -110,9 +123,9 @@ func TestCreateTxInsufficientFundsError(t *testing.T) {
cfg = &config{DisallowFree: false} cfg = &config{DisallowFree: false}
outputs := map[string]btcutil.Amount{outAddr1: 10, outAddr2: 1e9} outputs := map[string]btcutil.Amount{outAddr1: 10, outAddr2: 1e9}
eligible := eligibleInputsFromTx(t, txInfo.hex, []uint32{1}) eligible := eligibleInputsFromTx(t, txInfo.hex, []uint32{1})
bs := &keystore.BlockStamp{Height: 11111} bs := &waddrmgr.BlockStamp{Height: 11111}
changeAddr, _ := btcutil.DecodeAddress("muqW4gcixv58tVbSKRC5q6CRKy8RmyLgZ5", activeNet.Params) changeAddr, _ := btcutil.DecodeAddress("muqW4gcixv58tVbSKRC5q6CRKy8RmyLgZ5", activeNet.Params)
var tstChangeAddress = func(bs *keystore.BlockStamp) (btcutil.Address, error) { var tstChangeAddress = func(bs *waddrmgr.BlockStamp) (btcutil.Address, error) {
return changeAddr, nil return changeAddr, nil
} }
@ -150,28 +163,47 @@ func checkOutputsMatch(t *testing.T, msgtx *wire.MsgTx, expected map[string]btcu
} }
} }
// newKeyStore creates a new keystore and imports the given privKey into it. // newManager creates a new waddrmgr and imports the given privKey into it.
func newKeyStore(t *testing.T, privKeys []string, bs *keystore.BlockStamp) *keystore.Store { func newManager(t *testing.T, privKeys []string, bs *waddrmgr.BlockStamp) *waddrmgr.Manager {
passphrase := []byte{0, 1} dbPath := filepath.Join(os.TempDir(), "wallet.bin")
keys, err := keystore.New("/tmp/keys.bin", "Default acccount", passphrase, os.Remove(dbPath)
activeNet.Params, bs) db, err := walletdb.Create("bdb", dbPath)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
namespace, err := db.Namespace(waddrmgrNamespaceKey)
if err != nil {
t.Fatal(err)
}
seed, err := hdkeychain.GenerateSeed(hdkeychain.RecommendedSeedLen)
if err != nil {
t.Fatal(err)
}
pubPassphrase := []byte("pub")
privPassphrase := []byte("priv")
mgr, err := waddrmgr.Create(namespace, seed, pubPassphrase,
privPassphrase, activeNet.Params, fastScrypt)
if err != nil {
t.Fatal(err)
}
for _, key := range privKeys { for _, key := range privKeys {
wif, err := btcutil.DecodeWIF(key) wif, err := btcutil.DecodeWIF(key)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err = keys.Unlock(passphrase); err != nil { if err = mgr.Unlock(privPassphrase); err != nil {
t.Fatal(err) t.Fatal(err)
} }
_, err = keys.ImportPrivateKey(wif, bs) _, err = mgr.ImportPrivateKey(wif, bs)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
} }
return keys return mgr
} }
// eligibleInputsFromTx decodes the given txHex and returns the outputs with // eligibleInputsFromTx decodes the given txHex and returns the outputs with

View file

@ -20,7 +20,7 @@ import (
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil"
"github.com/btcsuite/btcwallet/chain" "github.com/btcsuite/btcwallet/chain"
"github.com/btcsuite/btcwallet/legacy/keystore" "github.com/btcsuite/btcwallet/waddrmgr"
) )
// RescanProgressMsg reports the current progress made by a rescan for a // RescanProgressMsg reports the current progress made by a rescan for a
@ -47,7 +47,7 @@ type RescanJob struct {
InitialSync bool InitialSync bool
Addrs []btcutil.Address Addrs []btcutil.Address
OutPoints []*wire.OutPoint OutPoints []*wire.OutPoint
BlockStamp keystore.BlockStamp BlockStamp waddrmgr.BlockStamp
err chan error err chan error
} }
@ -57,7 +57,7 @@ type rescanBatch struct {
initialSync bool initialSync bool
addrs []btcutil.Address addrs []btcutil.Address
outpoints []*wire.OutPoint outpoints []*wire.OutPoint
bs keystore.BlockStamp bs waddrmgr.BlockStamp
errChans []chan error errChans []chan error
} }
@ -172,9 +172,8 @@ out:
w.wg.Done() w.wg.Done()
} }
// rescanProgressHandler handles notifications for paritally and fully completed // rescanProgressHandler handles notifications for partially and fully completed
// rescans by marking each rescanned address as partially or fully synced and // rescans by marking each rescanned address as partially or fully synced.
// writing the keystore back to disk.
func (w *Wallet) rescanProgressHandler() { func (w *Wallet) rescanProgressHandler() {
out: out:
for { for {
@ -187,21 +186,14 @@ out:
log.Infof("Rescanned through block %v (height %d)", log.Infof("Rescanned through block %v (height %d)",
n.Hash, n.Height) n.Hash, n.Height)
// TODO(jrick): save partial syncs should also include bs := waddrmgr.BlockStamp{
// the block hash. Hash: *n.Hash,
for _, addr := range msg.Addresses { Height: n.Height,
err := w.KeyStore.SetSyncStatus(addr,
keystore.PartialSync(n.Height))
if err != nil {
log.Errorf("Error marking address %v "+
"partially synced: %v", addr, err)
} }
} if err := w.Manager.SetSyncedTo(&bs); err != nil {
w.KeyStore.MarkDirty() log.Errorf("Failed to update address manager "+
err := w.KeyStore.WriteIfDirty() "sync state for hash %v (height %d): %v",
if err != nil { n.Hash, n.Height, err)
log.Errorf("Could not write partial rescan "+
"progress to keystore: %v", err)
} }
case msg := <-w.rescanFinished: case msg := <-w.rescanFinished:
@ -211,11 +203,17 @@ out:
if msg.WasInitialSync { if msg.WasInitialSync {
w.ResendUnminedTxs() w.ResendUnminedTxs()
bs := keystore.BlockStamp{ bs := waddrmgr.BlockStamp{
Hash: n.Hash, Hash: *n.Hash,
Height: n.Height, Height: n.Height,
} }
w.KeyStore.SetSyncedWith(&bs) err := w.Manager.SetSyncedTo(&bs)
if err != nil {
log.Errorf("Failed to update address "+
"manager sync state for hash "+
"%v (height %d): %v", n.Hash,
n.Height, err)
}
w.notifyConnectedBlock(bs) w.notifyConnectedBlock(bs)
// Mark wallet as synced to chain so connected // Mark wallet as synced to chain so connected
@ -227,21 +225,6 @@ out:
"%s, height %d)", len(addrs), noun, n.Hash, "%s, height %d)", len(addrs), noun, n.Hash,
n.Height) n.Height)
for _, addr := range addrs {
err := w.KeyStore.SetSyncStatus(addr,
keystore.FullSync{})
if err != nil {
log.Errorf("Error marking address %v "+
"fully synced: %v", addr, err)
}
}
w.KeyStore.MarkDirty()
err := w.KeyStore.WriteIfDirty()
if err != nil {
log.Errorf("Could not write finished rescan "+
"progress to keystore: %v", err)
}
case <-w.quit: case <-w.quit:
break out break out
} }
@ -260,7 +243,7 @@ func (w *Wallet) rescanRPCHandler() {
log.Infof("Started rescan from block %v (height %d) for %d %s", log.Infof("Started rescan from block %v (height %d) for %d %s",
batch.bs.Hash, batch.bs.Height, numAddrs, noun) batch.bs.Hash, batch.bs.Height, numAddrs, noun)
err := w.chainSvr.Rescan(batch.bs.Hash, batch.addrs, err := w.chainSvr.Rescan(&batch.bs.Hash, batch.addrs,
batch.outpoints) batch.outpoints)
if err != nil { if err != nil {
log.Errorf("Rescan for %d %s failed: %v", numAddrs, log.Errorf("Rescan for %d %s failed: %v", numAddrs,
@ -271,34 +254,24 @@ func (w *Wallet) rescanRPCHandler() {
w.wg.Done() w.wg.Done()
} }
// RescanActiveAddresses begins a rescan for all active addresses of a // RescanActiveAddresses begins a rescan for all active addresses of a wallet.
// wallet. This is intended to be used to sync a wallet back up to the // This is intended to be used to sync a wallet back up to the current best
// current best block in the main chain, and is considered an intial sync // block in the main chain, and is considered an initial sync rescan.
// rescan. func (w *Wallet) RescanActiveAddresses() error {
func (w *Wallet) RescanActiveAddresses() (err error) { addrs, err := w.Manager.AllActiveAddresses()
// Determine the block necesary to start the rescan for all active
// addresses.
hash, height := w.KeyStore.SyncedTo()
if hash == nil {
// TODO: fix our "synced to block" handling (either in
// keystore or txstore, or elsewhere) so this *always*
// returns the block hash. Looking it up by height is
// asking for problems.
hash, err = w.chainSvr.GetBlockHash(int64(height))
if err != nil { if err != nil {
return return err
}
} }
actives := w.KeyStore.SortedActiveAddresses() // in case there are no addresses, we can skip queuing the rescan job
addrs := make([]btcutil.Address, len(actives)) if len(addrs) == 0 {
for i, addr := range actives { close(w.chainSynced)
addrs[i] = addr.Address() return nil
} }
unspents, err := w.TxStore.UnspentOutputs() unspents, err := w.TxStore.UnspentOutputs()
if err != nil { if err != nil {
return return err
} }
outpoints := make([]*wire.OutPoint, len(unspents)) outpoints := make([]*wire.OutPoint, len(unspents))
for i, output := range unspents { for i, output := range unspents {
@ -309,7 +282,7 @@ func (w *Wallet) RescanActiveAddresses() (err error) {
InitialSync: true, InitialSync: true,
Addrs: addrs, Addrs: addrs,
OutPoints: outpoints, OutPoints: outpoints,
BlockStamp: keystore.BlockStamp{Hash: hash, Height: height}, BlockStamp: w.Manager.SyncedTo(),
} }
// Submit merged job and block until rescan completes. // Submit merged job and block until rescan completes.

View file

@ -45,8 +45,8 @@ import (
"github.com/btcsuite/btcrpcclient" "github.com/btcsuite/btcrpcclient"
"github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil"
"github.com/btcsuite/btcwallet/chain" "github.com/btcsuite/btcwallet/chain"
"github.com/btcsuite/btcwallet/legacy/keystore"
"github.com/btcsuite/btcwallet/txstore" "github.com/btcsuite/btcwallet/txstore"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/websocket" "github.com/btcsuite/websocket"
) )
@ -165,6 +165,31 @@ func (c *websocketClient) send(b []byte) error {
} }
} }
// isManagerLockedError returns whether or not the passed error is due to the
// address manager being locked.
func isManagerLockedError(err error) bool {
merr, ok := err.(waddrmgr.ManagerError)
return ok && merr.ErrorCode == waddrmgr.ErrLocked
}
// isManagerWrongPassphraseError returns whether or not the passed error is due
// to the address manager being provided with an invalid passprhase.
func isManagerWrongPassphraseError(err error) bool {
merr, ok := err.(waddrmgr.ManagerError)
return ok && merr.ErrorCode == waddrmgr.ErrWrongPassphrase
}
// isManagerDuplicateError returns whether or not the passed error is due to a
// duplicate item being provided to the address manager.
func isManagerDuplicateError(err error) bool {
merr, ok := err.(waddrmgr.ManagerError)
if !ok {
return false
}
return merr.ErrorCode == waddrmgr.ErrDuplicate
}
// parseListeners splits the list of listen addresses passed in addrs into // parseListeners splits the list of listen addresses passed in addrs into
// IPv4 and IPv6 slices and returns them. This allows easy creation of the // IPv4 and IPv6 slices and returns them. This allows easy creation of the
// listeners on the correct interface "tcp4" and "tcp6". It also properly // listeners on the correct interface "tcp4" and "tcp6". It also properly
@ -265,13 +290,13 @@ type rpcServer struct {
// Channels read from other components from which notifications are // Channels read from other components from which notifications are
// created. // created.
connectedBlocks <-chan keystore.BlockStamp connectedBlocks <-chan waddrmgr.BlockStamp
disconnectedBlocks <-chan keystore.BlockStamp disconnectedBlocks <-chan waddrmgr.BlockStamp
newCredits <-chan txstore.Credit newCredits <-chan txstore.Credit
newDebits <-chan txstore.Debits newDebits <-chan txstore.Debits
minedCredits <-chan txstore.Credit minedCredits <-chan txstore.Credit
minedDebits <-chan txstore.Debits minedDebits <-chan txstore.Debits
keystoreLocked <-chan bool managerLocked <-chan bool
confirmedBalance <-chan btcutil.Amount confirmedBalance <-chan btcutil.Amount
unconfirmedBalance <-chan btcutil.Amount unconfirmedBalance <-chan btcutil.Amount
chainServerConnected <-chan bool chainServerConnected <-chan bool
@ -576,8 +601,8 @@ func (s *rpcServer) SetChainServer(chainSvr *chain.Client) {
// a chain server request that is handled by passing the request down to btcd. // a chain server request that is handled by passing the request down to btcd.
// //
// NOTE: These handlers do not handle special cases, such as the authenticate // NOTE: These handlers do not handle special cases, such as the authenticate
// and createencryptedwallet methods. Each of these must be checked // method. Each of these must be checked beforehand (the method is already
// beforehand (the method is already known) and handled accordingly. // known) and handled accordingly.
func (s *rpcServer) HandlerClosure(method string) requestHandlerClosure { func (s *rpcServer) HandlerClosure(method string) requestHandlerClosure {
s.handlerLock.Lock() s.handlerLock.Lock()
defer s.handlerLock.Unlock() defer s.handlerLock.Unlock()
@ -800,19 +825,6 @@ out:
} }
switch raw.Method { switch raw.Method {
case "createencryptedwallet":
result, err := s.handleCreateEncryptedWallet(request)
resp := makeResponse(raw.ID, result, err)
mresp, err := json.Marshal(resp)
// Expected to never fail.
if err != nil {
panic(err)
}
err = wsc.send(mresp)
if err != nil {
break out
}
case "stop": case "stop":
s.Stop() s.Stop()
resp := makeResponse(raw.ID, resp := makeResponse(raw.ID,
@ -987,16 +999,12 @@ func (s *rpcServer) PostClientRPC(w http.ResponseWriter, r *http.Request) {
} }
// Create the response and error from the request. Three special cases // Create the response and error from the request. Three special cases
// are handled for the authenticate, createencryptedwallet, and stop // are handled for the authenticate and stop request methods.
// request methods.
var resp btcjson.Reply var resp btcjson.Reply
switch raw.Method { switch raw.Method {
case "authenticate": case "authenticate":
// Drop it. // Drop it.
return return
case "createencryptedwallet":
result, err := s.handleCreateEncryptedWallet(rpcRequest)
resp = makeResponse(raw.ID, result, err)
case "stop": case "stop":
s.Stop() s.Stop()
resp = makeResponse(raw.ID, "btcwallet stopping.", nil) resp = makeResponse(raw.ID, "btcwallet stopping.", nil)
@ -1024,13 +1032,13 @@ type (
notificationCmds(w *Wallet) []btcjson.Cmd notificationCmds(w *Wallet) []btcjson.Cmd
} }
blockConnected keystore.BlockStamp blockConnected waddrmgr.BlockStamp
blockDisconnected keystore.BlockStamp blockDisconnected waddrmgr.BlockStamp
txCredit txstore.Credit txCredit txstore.Credit
txDebit txstore.Debits txDebit txstore.Debits
keystoreLocked bool managerLocked bool
confirmedBalance btcutil.Amount confirmedBalance btcutil.Amount
unconfirmedBalance btcutil.Amount unconfirmedBalance btcutil.Amount
@ -1085,8 +1093,8 @@ func (d txDebit) notificationCmds(w *Wallet) []btcjson.Cmd {
return ns return ns
} }
func (kl keystoreLocked) notificationCmds(w *Wallet) []btcjson.Cmd { func (l managerLocked) notificationCmds(w *Wallet) []btcjson.Cmd {
n := btcws.NewWalletLockStateNtfn("", bool(kl)) n := btcws.NewWalletLockStateNtfn("", bool(l))
return []btcjson.Cmd{n} return []btcjson.Cmd{n}
} }
@ -1123,8 +1131,8 @@ out:
s.enqueueNotification <- txCredit(n) s.enqueueNotification <- txCredit(n)
case n := <-s.minedDebits: case n := <-s.minedDebits:
s.enqueueNotification <- txDebit(n) s.enqueueNotification <- txDebit(n)
case n := <-s.keystoreLocked: case n := <-s.managerLocked:
s.enqueueNotification <- keystoreLocked(n) s.enqueueNotification <- managerLocked(n)
case n := <-s.confirmedBalance: case n := <-s.confirmedBalance:
s.enqueueNotification <- confirmedBalance(n) s.enqueueNotification <- confirmedBalance(n)
case n := <-s.unconfirmedBalance: case n := <-s.unconfirmedBalance:
@ -1173,9 +1181,9 @@ out:
"debit notifications: %v", err) "debit notifications: %v", err)
continue continue
} }
keystoreLocked, err := s.wallet.ListenKeystoreLockStatus() managerLocked, err := s.wallet.ListenLockStatus()
if err != nil { if err != nil {
log.Errorf("Could not register for keystore "+ log.Errorf("Could not register for manager "+
"lock state changes: %v", err) "lock state changes: %v", err)
continue continue
} }
@ -1197,7 +1205,7 @@ out:
s.newDebits = newDebits s.newDebits = newDebits
s.minedCredits = minedCredits s.minedCredits = minedCredits
s.minedDebits = minedDebits s.minedDebits = minedDebits
s.keystoreLocked = keystoreLocked s.managerLocked = managerLocked
s.confirmedBalance = confirmedBalance s.confirmedBalance = confirmedBalance
s.unconfirmedBalance = unconfirmedBalance s.unconfirmedBalance = unconfirmedBalance
@ -1248,7 +1256,7 @@ func (s *rpcServer) drainNotifications() {
// notificationQueue manages an infinitly-growing queue of notifications that // notificationQueue manages an infinitly-growing queue of notifications that
// wallet websocket clients may be interested in. It quits when the // wallet websocket clients may be interested in. It quits when the
// enqueueNotifiation channel is closed, dropping any still pending // enqueueNotification channel is closed, dropping any still pending
// notifications. // notifications.
func (s *rpcServer) notificationQueue() { func (s *rpcServer) notificationQueue() {
var q []wsClientNotification var q []wsClientNotification
@ -1403,7 +1411,6 @@ var rpcHandlers = map[string]requestHandler{
"getunconfirmedbalance": GetUnconfirmedBalance, "getunconfirmedbalance": GetUnconfirmedBalance,
"listaddresstransactions": ListAddressTransactions, "listaddresstransactions": ListAddressTransactions,
"listalltransactions": ListAllTransactions, "listalltransactions": ListAllTransactions,
"recoveraddresses": RecoverAddresses,
"walletislocked": WalletIsLocked, "walletislocked": WalletIsLocked,
} }
@ -1549,12 +1556,12 @@ func makeMultiSigScript(w *Wallet, keys []string, nRequired int) ([]byte, error)
case *btcutil.AddressPubKey: case *btcutil.AddressPubKey:
keysesPrecious[i] = addr keysesPrecious[i] = addr
case *btcutil.AddressPubKeyHash: case *btcutil.AddressPubKeyHash:
ainfo, err := w.KeyStore.Address(addr) ainfo, err := w.Manager.Address(addr)
if err != nil { if err != nil {
return nil, err return nil, err
} }
apkinfo := ainfo.(keystore.PubKeyAddress) apkinfo := ainfo.(waddrmgr.ManagedPubKeyAddress)
// This will be an addresspubkey // This will be an addresspubkey
a, err := btcutil.DecodeAddress(apkinfo.ExportPubKey(), a, err := btcutil.DecodeAddress(apkinfo.ExportPubKey(),
@ -1589,20 +1596,17 @@ func AddMultiSigAddress(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (in
} }
// TODO(oga) blockstamp current block? // TODO(oga) blockstamp current block?
address, err := w.KeyStore.ImportScript(script, bs := &waddrmgr.BlockStamp{
&keystore.BlockStamp{}) Hash: *activeNet.Params.GenesisHash,
Height: 0,
}
addr, err := w.Manager.ImportScript(script, bs)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Write wallet with imported multisig address to disk. return addr.Address().EncodeAddress(), nil
w.KeyStore.MarkDirty()
err = w.KeyStore.WriteIfDirty()
if err != nil {
return nil, fmt.Errorf("account write failed: %v", err)
}
return address.EncodeAddress(), nil
} }
// CreateMultiSig handles an createmultisig request by returning a // CreateMultiSig handles an createmultisig request by returning a
@ -1639,7 +1643,7 @@ func DumpPrivKey(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface
} }
key, err := w.DumpWIFPrivateKey(addr) key, err := w.DumpWIFPrivateKey(addr)
if err == keystore.ErrLocked { if isManagerLockedError(err) {
// Address was found, but the private key isn't // Address was found, but the private key isn't
// accessible. // accessible.
return nil, btcjson.ErrWalletUnlockNeeded return nil, btcjson.ErrWalletUnlockNeeded
@ -1652,9 +1656,10 @@ func DumpPrivKey(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface
// TODO: finish this to match bitcoind by writing the dump to a file. // TODO: finish this to match bitcoind by writing the dump to a file.
func DumpWallet(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { func DumpWallet(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) {
keys, err := w.DumpPrivKeys() keys, err := w.DumpPrivKeys()
if err == keystore.ErrLocked { if isManagerLockedError(err) {
return nil, btcjson.ErrWalletUnlockNeeded return nil, btcjson.ErrWalletUnlockNeeded
} }
return keys, err return keys, err
} }
@ -1671,12 +1676,7 @@ func ExportWatchingWallet(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (
return nil, err return nil, err
} }
wa, err := w.ExportWatchingWallet() return w.ExportWatchingWallet()
if err != nil {
return nil, err
}
return wa.exportBase64()
} }
// GetAddressesByAccount handles a getaddressesbyaccount request by returning // GetAddressesByAccount handles a getaddressesbyaccount request by returning
@ -1690,7 +1690,7 @@ func GetAddressesByAccount(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd)
return nil, err return nil, err
} }
return w.SortedActivePaymentAddresses(), nil return w.SortedActivePaymentAddresses()
} }
// GetBalance handles a getbalance request by returning the balance for an // GetBalance handles a getbalance request by returning the balance for an
@ -1733,7 +1733,9 @@ func GetInfo(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{},
return nil, err return nil, err
} }
info.WalletVersion = int32(keystore.VersCurrent.Uint32()) // TODO(davec): This should probably have a database version as opposed
// to using the manager version.
info.WalletVersion = int32(waddrmgr.LatestMgrVersion)
info.Balance = bal.ToUnit(btcutil.AmountBTC) info.Balance = bal.ToUnit(btcutil.AmountBTC)
// Keypool times are not tracked. set to current time. // Keypool times are not tracked. set to current time.
info.KeypoolOldest = time.Now().Unix() info.KeypoolOldest = time.Now().Unix()
@ -1759,7 +1761,7 @@ func GetAccount(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{
} }
// If it is in the wallet, we consider it part of the default account. // If it is in the wallet, we consider it part of the default account.
_, err = w.KeyStore.Address(addr) _, err = w.Manager.Address(addr)
if err != nil { if err != nil {
return nil, btcjson.ErrInvalidAddressOrKey return nil, btcjson.ErrInvalidAddressOrKey
} }
@ -1828,17 +1830,16 @@ func ImportPrivKey(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interfa
} }
// Import the private key, handling any errors. // Import the private key, handling any errors.
_, err = w.ImportPrivateKey(wif, &keystore.BlockStamp{}, cmd.Rescan) _, err = w.ImportPrivateKey(wif, nil, cmd.Rescan)
switch err { switch {
case keystore.ErrDuplicate: case isManagerDuplicateError(err):
// Do not return duplicate key errors to the client. // Do not return duplicate key errors to the client.
return nil, nil return nil, nil
case keystore.ErrLocked: case isManagerLockedError(err):
return nil, btcjson.ErrWalletUnlockNeeded return nil, btcjson.ErrWalletUnlockNeeded
default:
// If the import was successful, reply with nil.
return nil, err
} }
return nil, err
} }
// KeypoolRefill handles the keypoolrefill command. Since we handle the keypool // KeypoolRefill handles the keypoolrefill command. Since we handle the keypool
@ -2124,7 +2125,11 @@ func ListReceivedByAddress(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd)
if cmd.IncludeEmpty { if cmd.IncludeEmpty {
// Create an AddrData entry for each active address in the account. // Create an AddrData entry for each active address in the account.
// Otherwise we'll just get addresses from transactions later. // Otherwise we'll just get addresses from transactions later.
for _, address := range w.SortedActivePaymentAddresses() { sortedAddrs, err := w.SortedActivePaymentAddresses()
if err != nil {
return nil, err
}
for _, address := range sortedAddrs {
// There might be duplicates, just overwrite them. // There might be duplicates, just overwrite them.
allAddrData[address] = AddrData{} allAddrData[address] = AddrData{}
} }
@ -2352,14 +2357,14 @@ func sendPairs(w *Wallet, chainSvr *chain.Client, cmd btcjson.Cmd,
// was not successful. // was not successful.
createdTx, err := w.CreateSimpleTx(amounts, minconf) createdTx, err := w.CreateSimpleTx(amounts, minconf)
if err != nil { if err != nil {
switch err { switch {
case ErrNonPositiveAmount: case err == ErrNonPositiveAmount:
return nil, ErrNeedPositiveAmount return nil, ErrNeedPositiveAmount
case keystore.ErrLocked: case isManagerLockedError(err):
return nil, btcjson.ErrWalletUnlockNeeded return nil, btcjson.ErrWalletUnlockNeeded
default:
return nil, err
} }
return nil, err
} }
// Add to transaction store. // Add to transaction store.
@ -2492,17 +2497,16 @@ func SignMessage(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface
return nil, ParseError{err} return nil, ParseError{err}
} }
ainfo, err := w.KeyStore.Address(addr) ainfo, err := w.Manager.Address(addr)
if err != nil { if err != nil {
return nil, btcjson.ErrInvalidAddressOrKey return nil, btcjson.ErrInvalidAddressOrKey
} }
pka := ainfo.(keystore.PubKeyAddress) pka := ainfo.(waddrmgr.ManagedPubKeyAddress)
tmp, err := pka.PrivKey() privKey, err := pka.PrivKey()
if err != nil { if err != nil {
return nil, err return nil, err
} }
privKey := (*btcec.PrivateKey)(tmp)
fullmsg := "Bitcoin Signed Message:\n" + cmd.Message fullmsg := "Bitcoin Signed Message:\n" + cmd.Message
sigbytes, err := btcec.SignCompact(btcec.S256(), privKey, sigbytes, err := btcec.SignCompact(btcec.S256(), privKey,
@ -2514,71 +2518,6 @@ func SignMessage(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface
return base64.StdEncoding.EncodeToString(sigbytes), nil return base64.StdEncoding.EncodeToString(sigbytes), nil
} }
func (s *rpcServer) handleCreateEncryptedWallet(request []byte) (interface{}, error) {
s.handlerLock.Lock()
defer s.handlerLock.Unlock()
switch {
case s.wallet == nil && !s.createOK:
// Wallet hasn't finished loading, SetWallet (either with an
// actual or nil wallet) hasn't been called yet.
return nil, ErrUnloadedWallet
case s.wallet != nil:
return nil, errors.New("wallet already opened")
case s.chainSvr == nil:
return nil, ErrNeedsChainSvr
}
// Parse request to access the passphrase.
cmd, err := btcjson.ParseMarshaledCmd(request)
if err != nil {
return nil, err
}
req, ok := cmd.(*btcws.CreateEncryptedWalletCmd)
if !ok || len(req.Passphrase) == 0 {
// Request is already valid JSON-RPC and the method was good,
// so must be bad parameters.
return nil, btcjson.ErrInvalidParams
}
wallet, err := newEncryptedWallet([]byte(req.Passphrase), s.chainSvr)
if err != nil {
return nil, err
}
s.wallet = wallet
s.registerWalletNtfns <- struct{}{}
s.handlerLock = noopLocker{}
s.handlerLookup = lookupAnyHandler
wallet.Start(s.chainSvr)
// When the wallet eventually shuts down (i.e. from the stop RPC), close
// the rest of the server.
go func() {
wallet.WaitForShutdown()
s.Stop()
}()
// A nil reply is sent upon successful wallet creation.
return nil, nil
}
// RecoverAddresses recovers the next n addresses from an account's wallet.
func RecoverAddresses(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) {
cmd := icmd.(*btcws.RecoverAddressesCmd)
err := checkDefaultAccount(cmd.Account)
if err != nil {
return nil, err
}
err = w.RecoverAddresses(cmd.N)
return nil, err
}
// pendingTx is used for async fetching of transaction dependancies in // pendingTx is used for async fetching of transaction dependancies in
// SignRawTransaction. // SignRawTransaction.
type pendingTx struct { type pendingTx struct {
@ -2774,12 +2713,12 @@ func SignRawTransaction(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (in
} }
return wif.PrivKey, wif.CompressPubKey, nil return wif.PrivKey, wif.CompressPubKey, nil
} }
address, err := w.KeyStore.Address(addr) address, err := w.Manager.Address(addr)
if err != nil { if err != nil {
return nil, false, err return nil, false, err
} }
pka, ok := address.(keystore.PubKeyAddress) pka, ok := address.(waddrmgr.ManagedPubKeyAddress)
if !ok { if !ok {
return nil, false, errors.New("address is not " + return nil, false, errors.New("address is not " +
"a pubkey address") "a pubkey address")
@ -2805,20 +2744,17 @@ func SignRawTransaction(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (in
} }
return script, nil return script, nil
} }
address, err := w.KeyStore.Address(addr) address, err := w.Manager.Address(addr)
if err != nil { if err != nil {
return nil, err return nil, err
} }
sa, ok := address.(keystore.ScriptAddress) sa, ok := address.(waddrmgr.ManagedScriptAddress)
if !ok { if !ok {
return nil, errors.New("address is not a script" + return nil, errors.New("address is not a script" +
" address") " address")
} }
// TODO(oga) we could possible speed things up further return sa.Script()
// by returning the addresses, class and nrequired here
// thus avoiding recomputing them.
return sa.Script(), nil
}) })
// SigHashSingle inputs can only be signed if there's a // SigHashSingle inputs can only be signed if there's a
@ -2881,31 +2817,60 @@ func ValidateAddress(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (inter
result.Address = addr.EncodeAddress() result.Address = addr.EncodeAddress()
result.IsValid = true result.IsValid = true
ainfo, err := w.KeyStore.Address(addr) ainfo, err := w.Manager.Address(addr)
if err == nil { if managerErr, ok := err.(waddrmgr.ManagerError); ok {
if managerErr.ErrorCode == waddrmgr.ErrAddressNotFound {
// No additional information available about the address.
return result, nil
}
}
if err != nil {
return nil, err
}
// The address lookup was successful which means there is further
// information about it available and it is "mine".
result.IsMine = true result.IsMine = true
result.Account = "" result.Account = ""
if pka, ok := ainfo.(keystore.PubKeyAddress); ok { switch ma := ainfo.(type) {
result.IsCompressed = pka.Compressed() case waddrmgr.ManagedPubKeyAddress:
result.PubKey = pka.ExportPubKey() result.IsCompressed = ma.Compressed()
result.PubKey = ma.ExportPubKey()
} else if sa, ok := ainfo.(keystore.ScriptAddress); ok { case waddrmgr.ManagedScriptAddress:
result.IsScript = true result.IsScript = true
addresses := sa.Addresses()
addrStrings := make([]string, len(addresses)) // The script is only available if the manager is unlocked, so
for i, a := range addresses { // just break out now if there is an error.
script, err := ma.Script()
if err != nil {
break
}
result.Hex = hex.EncodeToString(script)
// This typically shouldn't fail unless an invalid script was
// imported. However, if it fails for any reason, there is no
// further information available, so just set the script type
// a non-standard and break out now.
class, addrs, reqSigs, err := txscript.ExtractPkScriptAddrs(
script, activeNet.Params)
if err != nil {
result.Script = txscript.NonStandardTy.String()
break
}
addrStrings := make([]string, len(addrs))
for i, a := range addrs {
addrStrings[i] = a.EncodeAddress() addrStrings[i] = a.EncodeAddress()
} }
result.Addresses = addrStrings result.Addresses = addrStrings
result.Hex = hex.EncodeToString(sa.Script())
class := sa.ScriptClass() // Multi-signature scripts also provide the number of required
// script type // signatures.
result.Script = class.String() result.Script = class.String()
if class == txscript.MultiSigTy { if class == txscript.MultiSigTy {
result.SigsRequired = int32(sa.RequiredSigs()) result.SigsRequired = int32(reqSigs)
}
} }
} }
@ -2992,7 +2957,7 @@ func WalletPassphraseChange(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd)
err := w.ChangePassphrase([]byte(cmd.OldPassphrase), err := w.ChangePassphrase([]byte(cmd.OldPassphrase),
[]byte(cmd.NewPassphrase)) []byte(cmd.NewPassphrase))
if err == keystore.ErrWrongPassphrase { if isManagerWrongPassphraseError(err) {
return nil, btcjson.ErrWalletPassphraseIncorrect return nil, btcjson.ErrWalletPassphraseIncorrect
} }
return nil, err return nil, err

View file

@ -6,6 +6,7 @@ import (
"encoding/binary" "encoding/binary"
"errors" "errors"
"io" "io"
"runtime/debug"
"golang.org/x/crypto/nacl/secretbox" "golang.org/x/crypto/nacl/secretbox"
"golang.org/x/crypto/scrypt" "golang.org/x/crypto/scrypt"
@ -122,6 +123,14 @@ func (sk *SecretKey) deriveKey(password *[]byte) error {
copy(sk.Key[:], key) copy(sk.Key[:], key)
zero(key) zero(key)
// I'm not a fan of forced garbage collections, but scrypt allocates a
// ton of memory and calling it back to back without a GC cycle in
// between means you end up needing twice the amount of memory. For
// example, if your scrypt parameters are such that you require 1GB and
// you call it twice in a row, without this you end up allocating 2GB
// since the first GB probably hasn't been released yet.
debug.FreeOSMemory()
return nil return nil
} }

View file

@ -50,6 +50,9 @@ var (
ScryptR: 8, ScryptR: 8,
ScryptP: 1, ScryptP: 1,
} }
// waddrmgrNamespaceKey is the namespace key for the waddrmgr package.
waddrmgrNamespaceKey = []byte("waddrmgrNamespace")
) )
// checkManagerError ensures the passed error is a ManagerError with an error // checkManagerError ensures the passed error is a ManagerError with an error
@ -88,7 +91,7 @@ func createDbNamespace(dbPath string) (walletdb.DB, walletdb.Namespace, error) {
return nil, nil, err return nil, nil, err
} }
namespace, err := db.Namespace([]byte("waddrmgr")) namespace, err := db.Namespace(waddrmgrNamespaceKey)
if err != nil { if err != nil {
db.Close() db.Close()
return nil, nil, err return nil, nil, err
@ -105,7 +108,7 @@ func openDbNamespace(dbPath string) (walletdb.DB, walletdb.Namespace, error) {
return nil, nil, err return nil, nil, err
} }
namespace, err := db.Namespace([]byte("waddrmgr")) namespace, err := db.Namespace(waddrmgrNamespaceKey)
if err != nil { if err != nil {
db.Close() db.Close()
return nil, nil, err return nil, nil, err

View file

@ -145,8 +145,8 @@ func defaultNewSecretKey(passphrase *[]byte, config *Options) (*snacl.SecretKey,
// paths. // paths.
var newSecretKey = defaultNewSecretKey var newSecretKey = defaultNewSecretKey
// EncryptorDecryptor provides an abstraction on top of snacl.CryptoKey so that our // EncryptorDecryptor provides an abstraction on top of snacl.CryptoKey so that
// tests can use dependency injection to force the behaviour they need. // our tests can use dependency injection to force the behaviour they need.
type EncryptorDecryptor interface { type EncryptorDecryptor interface {
Encrypt(in []byte) ([]byte, error) Encrypt(in []byte) ([]byte, error)
Decrypt(in []byte) ([]byte, error) Decrypt(in []byte) ([]byte, error)
@ -821,7 +821,7 @@ func (m *Manager) ChangePassphrase(oldPassphrase, newPassphrase []byte, private
// //
// Executing this function on a manager that is already watching-only will have // Executing this function on a manager that is already watching-only will have
// no effect. // no effect.
func (m *Manager) ConvertToWatchingOnly(pubPassphrase []byte) error { func (m *Manager) ConvertToWatchingOnly() error {
m.mtx.Lock() m.mtx.Lock()
defer m.mtx.Unlock() defer m.mtx.Unlock()

View file

@ -1168,7 +1168,7 @@ func testWatchingOnly(tc *testContext) bool {
tc.t.Errorf("%v", err) tc.t.Errorf("%v", err)
return false return false
} }
if err := mgr.ConvertToWatchingOnly(pubPassphrase); err != nil { if err := mgr.ConvertToWatchingOnly(); err != nil {
tc.t.Errorf("%v", err) tc.t.Errorf("%v", err)
return false return false
} }

519
wallet.go
View file

@ -22,7 +22,10 @@ import (
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt" "fmt"
"io/ioutil"
"os"
"path/filepath" "path/filepath"
"sort"
"sync" "sync"
"time" "time"
@ -32,8 +35,9 @@ import (
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil"
"github.com/btcsuite/btcwallet/chain" "github.com/btcsuite/btcwallet/chain"
"github.com/btcsuite/btcwallet/legacy/keystore"
"github.com/btcsuite/btcwallet/txstore" "github.com/btcsuite/btcwallet/txstore"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/walletdb"
) )
// ErrNotSynced describes an error where an operation cannot complete // ErrNotSynced describes an error where an operation cannot complete
@ -41,9 +45,22 @@ import (
// the remote chain server. // the remote chain server.
var ErrNotSynced = errors.New("wallet is not synchronized with the chain server") var ErrNotSynced = errors.New("wallet is not synchronized with the chain server")
var (
// waddrmgrNamespaceKey is the namespace key for the waddrmgr package.
waddrmgrNamespaceKey = []byte("waddrmgr")
)
const (
// defaultPubPassphrase is the default public wallet passphrase which is
// used when the user indicates they do not want additional protection
// provided by having all public data in the wallet encrypted by a
// passphrase only known to them.
defaultPubPassphrase = "public"
)
// networkDir returns the directory name of a network directory to hold wallet // networkDir returns the directory name of a network directory to hold wallet
// files. // files.
func networkDir(chainParams *chaincfg.Params) string { func networkDir(dataDir string, chainParams *chaincfg.Params) string {
netname := chainParams.Name netname := chainParams.Name
// For now, we must always name the testnet data directory as "testnet" // For now, we must always name the testnet data directory as "testnet"
@ -55,7 +72,7 @@ func networkDir(chainParams *chaincfg.Params) string {
netname = "testnet" netname = "testnet"
} }
return filepath.Join(cfg.DataDir, netname) return filepath.Join(dataDir, netname)
} }
// Wallet is a structure containing all the components for a // Wallet is a structure containing all the components for a
@ -63,7 +80,8 @@ func networkDir(chainParams *chaincfg.Params) string {
// addresses and keys), // addresses and keys),
type Wallet struct { type Wallet struct {
// Data stores // Data stores
KeyStore *keystore.Store db walletdb.DB
Manager *waddrmgr.Manager
TxStore *txstore.Store TxStore *txstore.Store
chainSvr *chain.Client chainSvr *chain.Client
@ -85,7 +103,7 @@ type Wallet struct {
// Channel for transaction creation requests. // Channel for transaction creation requests.
createTxRequests chan createTxRequest createTxRequests chan createTxRequest
// Channels for the keystore locker. // Channels for the manager locker.
unlockRequests chan unlockRequest unlockRequests chan unlockRequest
lockRequests chan struct{} lockRequests chan struct{}
holdUnlockRequests chan chan HeldUnlock holdUnlockRequests chan chan HeldUnlock
@ -95,8 +113,8 @@ type Wallet struct {
// Notification channels so other components can listen in on wallet // Notification channels so other components can listen in on wallet
// activity. These are initialized as nil, and must be created by // activity. These are initialized as nil, and must be created by
// calling one of the Listen* methods. // calling one of the Listen* methods.
connectedBlocks chan keystore.BlockStamp connectedBlocks chan waddrmgr.BlockStamp
disconnectedBlocks chan keystore.BlockStamp disconnectedBlocks chan waddrmgr.BlockStamp
lockStateChanges chan bool // true when locked lockStateChanges chan bool // true when locked
confirmedBalance chan btcutil.Amount confirmedBalance chan btcutil.Amount
unconfirmedBalance chan btcutil.Amount unconfirmedBalance chan btcutil.Amount
@ -106,11 +124,11 @@ type Wallet struct {
quit chan struct{} quit chan struct{}
} }
// newWallet creates a new Wallet structure with the provided key and // newWallet creates a new Wallet structure with the provided address manager
// transaction stores. // and transaction store.
func newWallet(keys *keystore.Store, txs *txstore.Store) *Wallet { func newWallet(mgr *waddrmgr.Manager, txs *txstore.Store) *Wallet {
return &Wallet{ return &Wallet{
KeyStore: keys, Manager: mgr,
TxStore: txs, TxStore: txs,
chainSvrLock: new(sync.Mutex), chainSvrLock: new(sync.Mutex),
chainSynced: make(chan struct{}), chainSynced: make(chan struct{}),
@ -158,14 +176,14 @@ func (w *Wallet) updateNotificationLock() {
// methods will block. // methods will block.
// //
// If this is called twice, ErrDuplicateListen is returned. // If this is called twice, ErrDuplicateListen is returned.
func (w *Wallet) ListenConnectedBlocks() (<-chan keystore.BlockStamp, error) { func (w *Wallet) ListenConnectedBlocks() (<-chan waddrmgr.BlockStamp, error) {
w.notificationLock.Lock() w.notificationLock.Lock()
defer w.notificationLock.Unlock() defer w.notificationLock.Unlock()
if w.connectedBlocks != nil { if w.connectedBlocks != nil {
return nil, ErrDuplicateListen return nil, ErrDuplicateListen
} }
w.connectedBlocks = make(chan keystore.BlockStamp) w.connectedBlocks = make(chan waddrmgr.BlockStamp)
w.updateNotificationLock() w.updateNotificationLock()
return w.connectedBlocks, nil return w.connectedBlocks, nil
} }
@ -175,25 +193,25 @@ func (w *Wallet) ListenConnectedBlocks() (<-chan keystore.BlockStamp, error) {
// block. // block.
// //
// If this is called twice, ErrDuplicateListen is returned. // If this is called twice, ErrDuplicateListen is returned.
func (w *Wallet) ListenDisconnectedBlocks() (<-chan keystore.BlockStamp, error) { func (w *Wallet) ListenDisconnectedBlocks() (<-chan waddrmgr.BlockStamp, error) {
w.notificationLock.Lock() w.notificationLock.Lock()
defer w.notificationLock.Unlock() defer w.notificationLock.Unlock()
if w.disconnectedBlocks != nil { if w.disconnectedBlocks != nil {
return nil, ErrDuplicateListen return nil, ErrDuplicateListen
} }
w.disconnectedBlocks = make(chan keystore.BlockStamp) w.disconnectedBlocks = make(chan waddrmgr.BlockStamp)
w.updateNotificationLock() w.updateNotificationLock()
return w.disconnectedBlocks, nil return w.disconnectedBlocks, nil
} }
// ListenKeystoreLockStatus returns a channel that passes the current lock state // ListenLockStatus returns a channel that passes the current lock state
// of the wallet keystore anytime the keystore is locked or unlocked. The value // of the wallet whenever the lock state is changed. The value is true for
// is true for locked, and false for unlocked. The channel must be read, or // locked, and false for unlocked. The channel must be read, or other wallet
// other wallet methods will block. // methods will block.
// //
// If this is called twice, ErrDuplicateListen is returned. // If this is called twice, ErrDuplicateListen is returned.
func (w *Wallet) ListenKeystoreLockStatus() (<-chan bool, error) { func (w *Wallet) ListenLockStatus() (<-chan bool, error) {
w.notificationLock.Lock() w.notificationLock.Lock()
defer w.notificationLock.Unlock() defer w.notificationLock.Unlock()
@ -239,7 +257,7 @@ func (w *Wallet) ListenUnconfirmedBalance() (<-chan btcutil.Amount, error) {
return w.unconfirmedBalance, nil return w.unconfirmedBalance, nil
} }
func (w *Wallet) notifyConnectedBlock(block keystore.BlockStamp) { func (w *Wallet) notifyConnectedBlock(block waddrmgr.BlockStamp) {
w.notificationLock.Lock() w.notificationLock.Lock()
if w.connectedBlocks != nil { if w.connectedBlocks != nil {
w.connectedBlocks <- block w.connectedBlocks <- block
@ -247,7 +265,7 @@ func (w *Wallet) notifyConnectedBlock(block keystore.BlockStamp) {
w.notificationLock.Unlock() w.notificationLock.Unlock()
} }
func (w *Wallet) notifyDisconnectedBlock(block keystore.BlockStamp) { func (w *Wallet) notifyDisconnectedBlock(block waddrmgr.BlockStamp) {
w.notificationLock.Lock() w.notificationLock.Lock()
if w.disconnectedBlocks != nil { if w.disconnectedBlocks != nil {
w.disconnectedBlocks <- block w.disconnectedBlocks <- block
@ -279,70 +297,6 @@ func (w *Wallet) notifyUnconfirmedBalance(bal btcutil.Amount) {
w.notificationLock.Unlock() w.notificationLock.Unlock()
} }
// openWallet opens a new wallet from disk.
func openWallet() (*Wallet, error) {
netdir := networkDir(activeNet.Params)
// Ensure that the network directory exists.
// TODO: move this?
if err := checkCreateDir(netdir); err != nil {
return nil, err
}
// Read key and transaction stores.
keys, err := keystore.OpenDir(netdir)
var txs *txstore.Store
if err == nil {
txs, err = txstore.OpenDir(netdir)
}
if err != nil {
// Special case: if the keystore was successfully read
// (keys != nil) but the transaction store was not, create a
// new txstore and write it out to disk. Write an unsynced
// wallet back to disk so on future opens, the empty txstore
// is not considered fully synced.
if keys == nil {
return nil, err
}
txs = txstore.New(netdir)
txs.MarkDirty()
err = txs.WriteIfDirty()
if err != nil {
return nil, err
}
keys.SetSyncedWith(nil)
keys.MarkDirty()
err = keys.WriteIfDirty()
if err != nil {
return nil, err
}
}
log.Infof("Opened wallet files") // TODO: log balance? last sync height?
return newWallet(keys, txs), nil
}
// newEncryptedWallet creates a new wallet encrypted with the provided
// passphrase.
func newEncryptedWallet(passphrase []byte, chainSvr *chain.Client) (*Wallet, error) {
// Get current block's height and hash.
bs, err := chainSvr.BlockStamp()
if err != nil {
return nil, err
}
// Create new wallet in memory.
keys, err := keystore.New(networkDir(activeNet.Params), "Default acccount",
passphrase, activeNet.Params, bs)
if err != nil {
return nil, err
}
w := newWallet(keys, txstore.New(networkDir(activeNet.Params)))
return w, nil
}
// Start starts the goroutines necessary to manage a wallet. // Start starts the goroutines necessary to manage a wallet.
func (w *Wallet) Start(chainServer *chain.Client) { func (w *Wallet) Start(chainServer *chain.Client) {
select { select {
@ -361,7 +315,7 @@ func (w *Wallet) Start(chainServer *chain.Client) {
go w.diskWriter() go w.diskWriter()
go w.handleChainNotifications() go w.handleChainNotifications()
go w.txCreator() go w.txCreator()
go w.keystoreLocker() go w.walletLocker()
go w.rescanBatchHandler() go w.rescanBatchHandler()
go w.rescanProgressHandler() go w.rescanProgressHandler()
go w.rescanRPCHandler() go w.rescanRPCHandler()
@ -429,7 +383,7 @@ func (w *Wallet) WaitForChainSync() {
// SyncedChainTip returns the hash and height of the block of the most // SyncedChainTip returns the hash and height of the block of the most
// recently seen block in the main chain. It returns errors if the // recently seen block in the main chain. It returns errors if the
// wallet has not yet been marked as synched with the chain. // wallet has not yet been marked as synched with the chain.
func (w *Wallet) SyncedChainTip() (*keystore.BlockStamp, error) { func (w *Wallet) SyncedChainTip() (*waddrmgr.BlockStamp, error) {
select { select {
case <-w.chainSynced: case <-w.chainSynced:
return w.chainSvr.BlockStamp() return w.chainSvr.BlockStamp()
@ -452,13 +406,13 @@ func (w *Wallet) syncWithChain() (err error) {
// Check that there was not any reorgs done since last connection. // Check that there was not any reorgs done since last connection.
// If so, rollback and rescan to catch up. // If so, rollback and rescan to catch up.
iter := w.KeyStore.NewIterateRecentBlocks() iter := w.Manager.NewIterateRecentBlocks()
for cont := iter != nil; cont; cont = iter.Prev() { for cont := iter != nil; cont; cont = iter.Prev() {
bs := iter.BlockStamp() bs := iter.BlockStamp()
log.Debugf("Checking for previous saved block with height %v hash %v", log.Debugf("Checking for previous saved block with height %v hash %v",
bs.Height, bs.Hash) bs.Height, bs.Hash)
if _, err := w.chainSvr.GetBlock(bs.Hash); err != nil { if _, err := w.chainSvr.GetBlock(&bs.Hash); err != nil {
continue continue
} }
@ -468,7 +422,7 @@ func (w *Wallet) syncWithChain() (err error) {
// returns true), then rollback the next and all child blocks. // returns true), then rollback the next and all child blocks.
if iter.Next() { if iter.Next() {
bs := iter.BlockStamp() bs := iter.BlockStamp()
w.KeyStore.SetSyncedWith(&bs) w.Manager.SetSyncedTo(&bs)
err = w.TxStore.Rollback(bs.Height) err = w.TxStore.Rollback(bs.Height)
if err != nil { if err != nil {
return return
@ -556,15 +510,15 @@ type (
HeldUnlock chan struct{} HeldUnlock chan struct{}
) )
// keystoreLocker manages the locked/unlocked state of a wallet. // walletLocker manages the locked/unlocked state of a wallet.
func (w *Wallet) keystoreLocker() { func (w *Wallet) walletLocker() {
var timeout <-chan time.Time var timeout <-chan time.Time
holdChan := make(HeldUnlock) holdChan := make(HeldUnlock)
out: out:
for { for {
select { select {
case req := <-w.unlockRequests: case req := <-w.unlockRequests:
err := w.KeyStore.Unlock(req.passphrase) err := w.Manager.Unlock(req.passphrase)
if err != nil { if err != nil {
req.err <- err req.err <- err
continue continue
@ -579,23 +533,12 @@ out:
continue continue
case req := <-w.changePassphrase: case req := <-w.changePassphrase:
// Changing the passphrase requires an unlocked err := w.Manager.ChangePassphrase(req.old, req.new, true)
// keystore, and for the old passphrase to be correct.
// Lock the keystore and unlock with the old passphase
// check its validity.
_ = w.KeyStore.Lock()
w.notifyLockStateChange(true)
timeout = nil
err := w.KeyStore.Unlock(req.old)
if err == nil {
w.notifyLockStateChange(false)
err = w.KeyStore.ChangePassphrase(req.new)
}
req.err <- err req.err <- err
continue continue
case req := <-w.holdUnlockRequests: case req := <-w.holdUnlockRequests:
if w.KeyStore.IsLocked() { if w.Manager.IsLocked() {
close(req) close(req)
continue continue
} }
@ -615,7 +558,7 @@ out:
continue continue
} }
case w.lockState <- w.KeyStore.IsLocked(): case w.lockState <- w.Manager.IsLocked():
continue continue
case <-w.quit: case <-w.quit:
@ -626,19 +569,23 @@ out:
} }
// Select statement fell through by an explicit lock or the // Select statement fell through by an explicit lock or the
// timer expiring. Lock the keystores here. // timer expiring. Lock the manager here.
timeout = nil timeout = nil
if err := w.KeyStore.Lock(); err != nil { err := w.Manager.Lock()
if err != nil {
log.Errorf("Could not lock wallet: %v", err) log.Errorf("Could not lock wallet: %v", err)
} } else {
w.notifyLockStateChange(true) w.notifyLockStateChange(true)
} }
}
w.wg.Done() w.wg.Done()
} }
// Unlock unlocks the wallet's keystore and locks the wallet again after // Unlock unlocks the wallet's address manager and relocks it after timeout has
// timeout has expired. If the wallet is already unlocked and the new // expired. If the wallet is already unlocked and the new passphrase is
// passphrase is correct, the current timeout is replaced with the new one. // correct, the current timeout is replaced with the new one. The wallet will
// be locked if the passphrase is incorrect or any other error occurs during the
// unlock.
func (w *Wallet) Unlock(passphrase []byte, timeout time.Duration) error { func (w *Wallet) Unlock(passphrase []byte, timeout time.Duration) error {
err := make(chan error, 1) err := make(chan error, 1)
w.unlockRequests <- unlockRequest{ w.unlockRequests <- unlockRequest{
@ -649,12 +596,12 @@ func (w *Wallet) Unlock(passphrase []byte, timeout time.Duration) error {
return <-err return <-err
} }
// Lock locks the wallet's keystore. // Lock locks the wallet's address manager.
func (w *Wallet) Lock() { func (w *Wallet) Lock() {
w.lockRequests <- struct{}{} w.lockRequests <- struct{}{}
} }
// Locked returns whether the keystore for a wallet is locked. // Locked returns whether the account manager for a wallet is locked.
func (w *Wallet) Locked() bool { func (w *Wallet) Locked() bool {
return <-w.lockState return <-w.lockState
} }
@ -670,7 +617,12 @@ func (w *Wallet) HoldUnlock() (HeldUnlock, error) {
w.holdUnlockRequests <- req w.holdUnlockRequests <- req
hl, ok := <-req hl, ok := <-req
if !ok { if !ok {
return nil, keystore.ErrLocked // TODO(davec): This should be defined and exported from
// waddrmgr.
return nil, waddrmgr.ManagerError{
ErrorCode: waddrmgr.ErrLocked,
Description: "address manager is locked",
}
} }
return hl, nil return hl, nil
} }
@ -683,8 +635,9 @@ func (c HeldUnlock) Release() {
} }
// ChangePassphrase attempts to change the passphrase for a wallet from old // ChangePassphrase attempts to change the passphrase for a wallet from old
// to new. Changing the passphrase is synchronized with all other keystore // to new. Changing the passphrase is synchronized with all other address
// locking and unlocking, and will result in a locked wallet on success. // manager locking and unlocking. The lock state will be the same as it was
// before the password change.
func (w *Wallet) ChangePassphrase(old, new []byte) error { func (w *Wallet) ChangePassphrase(old, new []byte) error {
err := make(chan error, 1) err := make(chan error, 1)
w.changePassphrase <- changePassphraseRequest{ w.changePassphrase <- changePassphraseRequest{
@ -695,8 +648,8 @@ func (w *Wallet) ChangePassphrase(old, new []byte) error {
return <-err return <-err
} }
// diskWriter periodically (every 10 seconds) writes out the key and transaction // diskWriter periodically (every 10 seconds) writes out the transaction store
// stores to disk if they are marked dirty. On shutdown, // to disk if it is marked dirty.
func (w *Wallet) diskWriter() { func (w *Wallet) diskWriter() {
ticker := time.NewTicker(10 * time.Second) ticker := time.NewTicker(10 * time.Second)
var wg sync.WaitGroup var wg sync.WaitGroup
@ -709,17 +662,9 @@ func (w *Wallet) diskWriter() {
done = true done = true
} }
log.Trace("Writing wallet files") log.Trace("Writing txstore")
wg.Add(2) wg.Add(1)
go func() {
err := w.KeyStore.WriteIfDirty()
if err != nil {
log.Errorf("Cannot write keystore: %v",
err)
}
wg.Done()
}()
go func() { go func() {
err := w.TxStore.WriteIfDirty() err := w.TxStore.WriteIfDirty()
if err != nil { if err != nil {
@ -741,7 +686,7 @@ func (w *Wallet) diskWriter() {
// a given address. Assumming correct TxStore usage, this will return true iff // a given address. Assumming correct TxStore usage, this will return true iff
// there are any transactions with outputs to this address in the blockchain or // there are any transactions with outputs to this address in the blockchain or
// the btcd mempool. // the btcd mempool.
func (w *Wallet) AddressUsed(addr btcutil.Address) bool { func (w *Wallet) AddressUsed(addr waddrmgr.ManagedAddress) bool {
// This not only can be optimized by recording this data as it is // This not only can be optimized by recording this data as it is
// read when opening a wallet, and keeping it up to date each time a // read when opening a wallet, and keeping it up to date each time a
// new received tx arrives, but it probably should in case an address is // new received tx arrives, but it probably should in case an address is
@ -754,7 +699,7 @@ func (w *Wallet) AddressUsed(addr btcutil.Address) bool {
// range below does nothing. // range below does nothing.
_, addrs, _, _ := c.Addresses(activeNet.Params) _, addrs, _, _ := c.Addresses(activeNet.Params)
for _, a := range addrs { for _, a := range addrs {
if addr.String() == a.String() { if addr.Address().String() == a.String() {
return true return true
} }
} }
@ -785,14 +730,17 @@ func (w *Wallet) CalculateBalance(confirms int) (btcutil.Amount, error) {
// one transaction spending to it in the blockchain or btcd mempool), the next // one transaction spending to it in the blockchain or btcd mempool), the next
// chained address is returned. // chained address is returned.
func (w *Wallet) CurrentAddress() (btcutil.Address, error) { func (w *Wallet) CurrentAddress() (btcutil.Address, error) {
addr := w.KeyStore.LastChainedAddress() addr, err := w.Manager.LastExternalAddress(0)
if err != nil {
return nil, err
}
// Get next chained address if the last one has already been used. // Get next chained address if the last one has already been used.
if w.AddressUsed(addr) { if w.AddressUsed(addr) {
return w.NewAddress() return w.NewAddress()
} }
return addr, nil return addr.Address(), nil
} }
// ListSinceBlock returns a slice of objects with details about transactions // ListSinceBlock returns a slice of objects with details about transactions
@ -816,7 +764,7 @@ func (w *Wallet) ListSinceBlock(since, curBlockHeight int32,
} }
jsonResults, err := txRecord.ToJSON("", curBlockHeight, jsonResults, err := txRecord.ToJSON("", curBlockHeight,
w.KeyStore.Net()) w.Manager.ChainParams())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -844,7 +792,7 @@ func (w *Wallet) ListTransactions(from, count int) ([]btcjson.ListTransactionsRe
// Search in reverse order: lookup most recently-added first. // Search in reverse order: lookup most recently-added first.
for i := len(records) - 1; i >= from && i >= lastLookupIdx; i-- { for i := len(records) - 1; i >= from && i >= lastLookupIdx; i-- {
jsonResults, err := records[i].ToJSON("", bs.Height, jsonResults, err := records[i].ToJSON("", bs.Height,
w.KeyStore.Net()) w.Manager.ChainParams())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -886,7 +834,7 @@ func (w *Wallet) ListAddressTransactions(pkHashes map[string]struct{}) (
continue continue
} }
jsonResult, err := c.ToJSON("", bs.Height, jsonResult, err := c.ToJSON("", bs.Height,
w.KeyStore.Net()) w.Manager.ChainParams())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -914,7 +862,7 @@ func (w *Wallet) ListAllTransactions() ([]btcjson.ListTransactionsResult, error)
records := w.TxStore.Records() records := w.TxStore.Records()
for i := len(records) - 1; i >= 0; i-- { for i := len(records) - 1; i >= 0; i-- {
jsonResults, err := records[i].ToJSON("", bs.Height, jsonResults, err := records[i].ToJSON("", bs.Height,
w.KeyStore.Net()) w.Manager.ChainParams())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -996,15 +944,26 @@ func (w *Wallet) ListUnspent(minconf, maxconf int,
// DumpPrivKeys returns the WIF-encoded private keys for all addresses with // DumpPrivKeys returns the WIF-encoded private keys for all addresses with
// private keys in a wallet. // private keys in a wallet.
func (w *Wallet) DumpPrivKeys() ([]string, error) { func (w *Wallet) DumpPrivKeys() ([]string, error) {
// Iterate over each active address, appending the private addrs, err := w.Manager.AllActiveAddresses()
// key to privkeys. if err != nil {
privkeys := []string{} return nil, err
for _, info := range w.KeyStore.ActiveAddresses() { }
// Iterate over each active address, appending the private key to
// privkeys.
privkeys := make([]string, 0, len(addrs))
for _, addr := range addrs {
ma, err := w.Manager.Address(addr)
if err != nil {
return nil, err
}
// Only those addresses with keys needed. // Only those addresses with keys needed.
pka, ok := info.(keystore.PubKeyAddress) pka, ok := ma.(waddrmgr.ManagedPubKeyAddress)
if !ok { if !ok {
continue continue
} }
wif, err := pka.ExportPrivKey() wif, err := pka.ExportPrivKey()
if err != nil { if err != nil {
// It would be nice to zero out the array here. However, // It would be nice to zero out the array here. However,
@ -1022,12 +981,12 @@ func (w *Wallet) DumpPrivKeys() ([]string, error) {
// single wallet address. // single wallet address.
func (w *Wallet) DumpWIFPrivateKey(addr btcutil.Address) (string, error) { func (w *Wallet) DumpWIFPrivateKey(addr btcutil.Address) (string, error) {
// Get private key from wallet if it exists. // Get private key from wallet if it exists.
address, err := w.KeyStore.Address(addr) address, err := w.Manager.Address(addr)
if err != nil { if err != nil {
return "", err return "", err
} }
pka, ok := address.(keystore.PubKeyAddress) pka, ok := address.(waddrmgr.ManagedPubKeyAddress)
if !ok { if !ok {
return "", fmt.Errorf("address %s is not a key type", addr) return "", fmt.Errorf("address %s is not a key type", addr)
} }
@ -1041,31 +1000,31 @@ func (w *Wallet) DumpWIFPrivateKey(addr btcutil.Address) (string, error) {
// ImportPrivateKey imports a private key to the wallet and writes the new // ImportPrivateKey imports a private key to the wallet and writes the new
// wallet to disk. // wallet to disk.
func (w *Wallet) ImportPrivateKey(wif *btcutil.WIF, bs *keystore.BlockStamp, func (w *Wallet) ImportPrivateKey(wif *btcutil.WIF, bs *waddrmgr.BlockStamp,
rescan bool) (string, error) { rescan bool) (string, error) {
// Attempt to import private key into wallet. // The starting block for the key is the genesis block unless otherwise
addr, err := w.KeyStore.ImportPrivateKey(wif, bs) // specified.
if err != nil { if bs == nil {
return "", err bs = &waddrmgr.BlockStamp{
Hash: *activeNet.Params.GenesisHash,
Height: 0,
}
} }
// Immediately write wallet to disk. // Attempt to import private key into wallet.
w.KeyStore.MarkDirty() addr, err := w.Manager.ImportPrivateKey(wif, bs)
if err := w.KeyStore.WriteIfDirty(); err != nil { if err != nil {
return "", fmt.Errorf("cannot write key: %v", err) return "", err
} }
// Rescan blockchain for transactions with txout scripts paying to the // Rescan blockchain for transactions with txout scripts paying to the
// imported address. // imported address.
if rescan { if rescan {
job := &RescanJob{ job := &RescanJob{
Addrs: []btcutil.Address{addr}, Addrs: []btcutil.Address{addr.Address()},
OutPoints: nil, OutPoints: nil,
BlockStamp: keystore.BlockStamp{ BlockStamp: *bs,
Hash: activeNet.Params.GenesisHash,
Height: 0,
},
} }
// Submit rescan job and log when the import has completed. // Submit rescan job and log when the import has completed.
@ -1075,42 +1034,86 @@ func (w *Wallet) ImportPrivateKey(wif *btcutil.WIF, bs *keystore.BlockStamp,
_ = w.SubmitRescan(job) _ = w.SubmitRescan(job)
} }
addrStr := addr.EncodeAddress() addrStr := addr.Address().EncodeAddress()
log.Infof("Imported payment address %s", addrStr) log.Infof("Imported payment address %s", addrStr)
// Return the payment address string of the imported private key. // Return the payment address string of the imported private key.
return addrStr, nil return addrStr, nil
} }
// ExportWatchingWallet returns the watching-only copy of a wallet. Both wallets // ExportWatchingWallet returns a watching-only version of the wallet serialized
// share the same tx store, so locking one will lock the other as well. The // in a map.
// returned wallet should be serialized and exported quickly, and then dropped func (w *Wallet) ExportWatchingWallet() (map[string]string, error) {
// from scope. tmpDir, err := ioutil.TempDir("", "btcwallet")
func (w *Wallet) ExportWatchingWallet() (*Wallet, error) {
ww, err := w.KeyStore.ExportWatchingWallet()
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer os.RemoveAll(tmpDir)
wa := *w // Create a new file and write a copy of the current database into it.
wa.KeyStore = ww woDbPath := filepath.Join(tmpDir, walletDbWatchingOnlyName)
return &wa, nil fi, err := os.OpenFile(woDbPath, os.O_CREATE|os.O_RDWR, 0600)
if err != nil {
return nil, err
}
if err := w.db.Copy(fi); err != nil {
fi.Close()
return nil, err
}
fi.Close()
defer os.Remove(woDbPath)
// Open the new database, get the address manager namespace, and open
// it.
woDb, err := walletdb.Open("bdb", woDbPath)
if err != nil {
_ = os.Remove(woDbPath)
return nil, err
}
defer woDb.Close()
namespace, err := woDb.Namespace(waddrmgrNamespaceKey)
if err != nil {
return nil, err
}
woMgr, err := waddrmgr.Open(namespace, []byte(cfg.WalletPass),
activeNet.Params, nil)
if err != nil {
return nil, err
}
defer woMgr.Close()
// Convert the namespace to watching only if needed.
if err := woMgr.ConvertToWatchingOnly(); err != nil {
// Only return the error is it's not because it's already
// watching-only. When it is already watching-only, the code
// just falls through to the export below.
if merr, ok := err.(waddrmgr.ManagerError); ok &&
merr.ErrorCode != waddrmgr.ErrWatchingOnly {
return nil, err
}
}
// Export the watching only wallet's serialized data.
woWallet := *w
woWallet.db = woDb
woWallet.Manager = woMgr
return woWallet.exportBase64()
} }
// exportBase64 exports a wallet's serialized key, and tx stores as // exportBase64 exports a wallet's serialized database and tx store as
// base64-encoded values in a map. // base64-encoded values in a map.
func (w *Wallet) exportBase64() (map[string]string, error) { func (w *Wallet) exportBase64() (map[string]string, error) {
buf := bytes.Buffer{} var buf bytes.Buffer
m := make(map[string]string) m := make(map[string]string)
_, err := w.KeyStore.WriteTo(&buf) if err := w.db.Copy(&buf); err != nil {
if err != nil {
return nil, err return nil, err
} }
m["wallet"] = base64.StdEncoding.EncodeToString(buf.Bytes()) m["wallet"] = base64.StdEncoding.EncodeToString(buf.Bytes())
buf.Reset() buf.Reset()
if _, err = w.TxStore.WriteTo(&buf); err != nil { if _, err := w.TxStore.WriteTo(&buf); err != nil {
return nil, err return nil, err
} }
m["tx"] = base64.StdEncoding.EncodeToString(buf.Bytes()) m["tx"] = base64.StdEncoding.EncodeToString(buf.Bytes())
@ -1180,114 +1183,62 @@ func (w *Wallet) ResendUnminedTxs() {
// SortedActivePaymentAddresses returns a slice of all active payment // SortedActivePaymentAddresses returns a slice of all active payment
// addresses in a wallet. // addresses in a wallet.
func (w *Wallet) SortedActivePaymentAddresses() []string { func (w *Wallet) SortedActivePaymentAddresses() ([]string, error) {
infos := w.KeyStore.SortedActiveAddresses() addrs, err := w.Manager.AllActiveAddresses()
if err != nil {
addrs := make([]string, len(infos)) return nil, err
for i, info := range infos {
addrs[i] = info.Address().EncodeAddress()
} }
return addrs addrStrs := make([]string, len(addrs))
for i, addr := range addrs {
addrStrs[i] = addr.EncodeAddress()
}
sort.Sort(sort.StringSlice(addrStrs))
return addrStrs, nil
} }
// NewAddress returns the next chained address for a wallet. // NewAddress returns the next external chained address for a wallet.
func (w *Wallet) NewAddress() (btcutil.Address, error) { func (w *Wallet) NewAddress() (btcutil.Address, error) {
// Get current block's height and hash.
bs, err := w.SyncedChainTip()
if err != nil {
return nil, err
}
// Get next address from wallet. // Get next address from wallet.
addr, err := w.KeyStore.NextChainedAddress(bs) account := uint32(0)
addrs, err := w.Manager.NextExternalAddresses(account, 1)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Immediately write updated wallet to disk.
w.KeyStore.MarkDirty()
if err := w.KeyStore.WriteIfDirty(); err != nil {
return nil, fmt.Errorf("key write failed: %v", err)
}
// Request updates from btcd for new transactions sent to this address. // Request updates from btcd for new transactions sent to this address.
if err := w.chainSvr.NotifyReceived([]btcutil.Address{addr}); err != nil { utilAddrs := make([]btcutil.Address, len(addrs))
for i, addr := range addrs {
utilAddrs[i] = addr.Address()
}
if err := w.chainSvr.NotifyReceived(utilAddrs); err != nil {
return nil, err return nil, err
} }
return addr, nil return utilAddrs[0], nil
} }
// NewChangeAddress returns a new change address for a wallet. // NewChangeAddress returns a new change address for a wallet.
func (w *Wallet) NewChangeAddress() (btcutil.Address, error) { func (w *Wallet) NewChangeAddress() (btcutil.Address, error) {
// Get current block's height and hash. // Get next chained change address from wallet for account 0.
bs, err := w.SyncedChainTip() account := uint32(0)
addrs, err := w.Manager.NextInternalAddresses(account, 1)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Get next chained change address from wallet.
addr, err := w.KeyStore.ChangeAddress(bs)
if err != nil {
return nil, err
}
// Immediately write updated wallet to disk.
w.KeyStore.MarkDirty()
if err := w.KeyStore.WriteIfDirty(); err != nil {
return nil, fmt.Errorf("key write failed: %v", err)
}
// Request updates from btcd for new transactions sent to this address. // Request updates from btcd for new transactions sent to this address.
if err := w.chainSvr.NotifyReceived([]btcutil.Address{addr}); err != nil { utilAddrs := make([]btcutil.Address, len(addrs))
for i, addr := range addrs {
utilAddrs[i] = addr.Address()
}
if err := w.chainSvr.NotifyReceived(utilAddrs); err != nil {
return nil, err return nil, err
} }
return addr, nil return utilAddrs[0], nil
}
// RecoverAddresses recovers the next n chained addresses of a wallet.
func (w *Wallet) RecoverAddresses(n int) error {
// Get info on the last chained address. The rescan starts at the
// earliest block height the last chained address might appear at.
last := w.KeyStore.LastChainedAddress()
lastInfo, err := w.KeyStore.Address(last)
if err != nil {
return err
}
addrs, err := w.KeyStore.ExtendActiveAddresses(n)
if err != nil {
return err
}
// Determine the block necesary to start the rescan.
height := lastInfo.FirstBlock()
// TODO: fix our "synced to block" handling (either in
// keystore or txstore, or elsewhere) so this *always*
// returns the block hash. Looking it up by height is
// asking for problems.
hash, err := w.chainSvr.GetBlockHash(int64(height))
if err != nil {
return err
}
// Run a goroutine to rescan blockchain for recovered addresses.
job := &RescanJob{
Addrs: addrs,
OutPoints: nil,
BlockStamp: keystore.BlockStamp{
Hash: hash,
Height: height,
},
}
// Begin rescan and do not wait for it to finish. Because the success
// or failure of the rescan is logged elsewhere and the returned channel
// does not need to be read, ignore the return value.
_ = w.SubmitRescan(job)
return nil
} }
// TotalReceived iterates through a wallet's transaction history, returning the // TotalReceived iterates through a wallet's transaction history, returning the
@ -1361,3 +1312,57 @@ func (w *Wallet) TxRecord(txSha *wire.ShaHash) (r *txstore.TxRecord, ok bool) {
} }
return nil, false return nil, false
} }
// openWallet opens a wallet from disk.
func openWallet() (*Wallet, error) {
netdir := networkDir(cfg.DataDir, activeNet.Params)
dbPath := filepath.Join(netdir, walletDbName)
// Ensure that the network directory exists.
if err := checkCreateDir(netdir); err != nil {
return nil, err
}
// Open the database using the boltdb backend.
db, err := walletdb.Open("bdb", dbPath)
if err != nil {
return nil, err
}
// Get the namespace for the address manager.
namespace, err := db.Namespace(waddrmgrNamespaceKey)
if err != nil {
return nil, err
}
// Open address manager and transaction store.
var txs *txstore.Store
mgr, err := waddrmgr.Open(namespace, []byte(cfg.WalletPass),
activeNet.Params, nil)
if err == nil {
txs, err = txstore.OpenDir(netdir)
}
if err != nil {
// Special case: if the address manager was successfully read
// (mgr != nil) but the transaction store was not, create a
// new txstore and write it out to disk. Write an unsynced
// manager back to disk so on future opens, the empty txstore
// is not considered fully synced.
if mgr == nil {
return nil, err
}
txs = txstore.New(netdir)
txs.MarkDirty()
err = txs.WriteIfDirty()
if err != nil {
return nil, err
}
mgr.SetSyncedTo(nil)
}
log.Infof("Opened wallet files") // TODO: log balance? last sync height?
wallet := newWallet(mgr, txs)
wallet.db = db
return wallet, nil
}

296
walletsetup.go Normal file
View file

@ -0,0 +1,296 @@
/*
* 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 main
import (
"bufio"
"encoding/hex"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/btcsuite/btcutil/hdkeychain"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/walletdb"
_ "github.com/btcsuite/btcwallet/walletdb/bdb"
)
// promptConsoleList prompts the user with the given prefix, list of valid
// responses, and default list entry to use. The function will repeat the
// prompt to the user until they enter a valid response.
func promptConsoleList(reader *bufio.Reader, prefix string, validResponses []string, defaultEntry string) (string, error) {
// Setup the prompt according to the parameters.
validStrings := strings.Join(validResponses, "/")
var prompt string
if defaultEntry != "" {
prompt = fmt.Sprintf("%s (%s) [%s]: ", prefix, validStrings,
defaultEntry)
} else {
prompt = fmt.Sprintf("%s (%s): ", prefix, validStrings)
}
// Prompt the user until one of the valid responses is given.
for {
fmt.Print(prompt)
reply, err := reader.ReadString('\n')
if err != nil {
return "", err
}
reply = strings.TrimSpace(strings.ToLower(reply))
if reply == "" {
reply = defaultEntry
}
for _, validResponse := range validResponses {
if reply == validResponse {
return reply, nil
}
}
}
}
// promptConsoleListBool prompts the user for a boolean (yes/no) with the given
// prefix. The function will repeat the prompt to the user until they enter a
// valid reponse.
func promptConsoleListBool(reader *bufio.Reader, prefix string, defaultEntry string) (bool, error) {
// Setup the valid responses.
valid := []string{"n", "no", "y", "yes"}
response, err := promptConsoleList(reader, prefix, valid, defaultEntry)
if err != nil {
return false, err
}
return response == "yes" || response == "y", nil
}
// promptConsolePass prompts the user for a passphrase with the given prefix.
// The function will ask the user to confirm the passphrase and will repeat
// the prompts until they enter a matching response.
func promptConsolePass(reader *bufio.Reader, prefix string) (string, error) {
// Prompt the user until they enter a passphrase.
prompt := fmt.Sprintf("%s: ", prefix)
for {
fmt.Print(prompt)
pass, err := reader.ReadString('\n')
if err != nil {
return "", err
}
pass = strings.TrimSpace(pass)
if pass == "" {
continue
}
fmt.Print("Confirm passphrase: ")
confirm, err := reader.ReadString('\n')
if err != nil {
return "", err
}
confirm = strings.TrimSpace(confirm)
if pass != confirm {
fmt.Println("The entered passphrases do not match")
continue
}
return pass, nil
}
}
// promptConsolePublicPass prompts the user whether they want to add an
// additional layer of encryption to the wallet. When the user answers yes and
// there is already a public passphrase provided via the passed config, it
// prompts them whether or not to use that configured passphrase. It will also
// detect when the same passphrase is used for the private and public passphrase
// and prompt the user if they are sure they want to use the same passphrase for
// both. Finally, all prompts are repeated until the user enters a valid
// response.
func promptConsolePublicPass(reader *bufio.Reader, privPass string, cfg *config) (string, error) {
pubPass := defaultPubPassphrase
usePubPass, err := promptConsoleListBool(reader, "Do you want "+
"to add an additional layer of encryption for public "+
"data?", "no")
if err != nil {
return "", err
}
if !usePubPass {
return pubPass, nil
}
if cfg.WalletPass != pubPass {
useExisting, err := promptConsoleListBool(reader, "Use the "+
"existing configured public passphrase for encryption "+
"of public data?", "no")
if err != nil {
return "", err
}
if useExisting {
return cfg.WalletPass, nil
}
}
for {
pubPass, err = promptConsolePass(reader, "Enter the public "+
"passphrase for your new wallet")
if err != nil {
return "", err
}
if pubPass == privPass {
useSamePass, err := promptConsoleListBool(reader,
"Are you sure want to use the same passphrase "+
"for public and private data?", "no")
if err != nil {
return "", err
}
if useSamePass {
break
}
continue
}
break
}
fmt.Println("NOTE: Use the --walletpass option to configure your " +
"public passphrase.")
return pubPass, nil
}
// promptConsoleSeed prompts the user whether they want to use an existing
// wallet generation seed. When the user answers no, a seed will be generated
// and displayed to the user along with prompting them for confirmation. When
// the user answers yes, a the user is prompted for it. All prompts are
// repeated until the user enters a valid response.
func promptConsoleSeed(reader *bufio.Reader) ([]byte, error) {
// Ascertain the wallet generation seed.
useUserSeed, err := promptConsoleListBool(reader, "Do you have an "+
"existing wallet seed you want to use?", "no")
if err != nil {
return nil, err
}
if !useUserSeed {
seed, err := hdkeychain.GenerateSeed(hdkeychain.RecommendedSeedLen)
if err != nil {
return nil, err
}
fmt.Println("Your wallet generation seed is:")
fmt.Printf("%x\n", seed)
fmt.Println("IMPORTANT: Keep the seed in a safe place as you\n" +
"will NOT be able to restore your wallet without it.")
fmt.Println("Please keep in mind that anyone who has access\n" +
"to the seed can also restore your wallet thereby\n" +
"giving them access to all your funds, so it is\n" +
"imperative that you keep it in a secure location.")
for {
fmt.Print(`Once you have stored the seed in a safe ` +
`and secure location, enter "OK" to continue: `)
confirmSeed, err := reader.ReadString('\n')
if err != nil {
return nil, err
}
confirmSeed = strings.TrimSpace(confirmSeed)
confirmSeed = strings.Trim(confirmSeed, `"`)
if confirmSeed == "OK" {
break
}
}
return seed, nil
}
for {
fmt.Print("Enter existing wallet seed: ")
seedStr, err := reader.ReadString('\n')
if err != nil {
return nil, err
}
seedStr = strings.TrimSpace(strings.ToLower(seedStr))
seed, err := hex.DecodeString(seedStr)
if err != nil || len(seed) < hdkeychain.MinSeedBytes ||
len(seed) > hdkeychain.MaxSeedBytes {
fmt.Printf("Invalid seed specified. Must be a "+
"hexadecimal value that is at least %d bits and "+
"at most %d bits\n", hdkeychain.MinSeedBytes*8,
hdkeychain.MaxSeedBytes*8)
continue
}
return seed, nil
}
}
// createWallet prompts the user for information needed to generate a new wallet
// and generates the wallet accordingly. The new wallet will reside at the
// provided path.
func createWallet(cfg *config) error {
// Start by prompting for the private passphrase.
reader := bufio.NewReader(os.Stdin)
privPass, err := promptConsolePass(reader, "Enter the private passphrase "+
"for your new wallet")
if err != nil {
return err
}
// Ascertain the public passphrase. This will either be a value
// specified by the user or the default hard-coded public passphrase if
// the user does not want the additional public data encryption.
pubPass, err := promptConsolePublicPass(reader, privPass, cfg)
if err != nil {
return err
}
// Ascertain the wallet generation seed. This will either be an
// automatically generated value the user has already confirmed or a
// value the user has entered which has already been validated.
seed, err := promptConsoleSeed(reader)
if err != nil {
return err
}
// Create the wallet.
netDir := networkDir(cfg.DataDir, activeNet.Params)
dbPath := filepath.Join(netDir, walletDbName)
fmt.Println("Creating the wallet...")
// Create the wallet database backed by bolt db.
db, err := walletdb.Create("bdb", dbPath)
if err != nil {
return err
}
// Create the address manager.
namespace, err := db.Namespace(waddrmgrNamespaceKey)
if err != nil {
return err
}
manager, err := waddrmgr.Create(namespace, seed, []byte(pubPass),
[]byte(privPass), activeNet.Params, nil)
if err != nil {
return err
}
manager.Close()
fmt.Println("The wallet has been created successfully.")
return nil
}