diff --git a/btcwallet.go b/btcwallet.go index 716ae43..b8cd0cd 100644 --- a/btcwallet.go +++ b/btcwallet.go @@ -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. // This will be updated with the wallet and chain server RPC client // created below after each is created. @@ -78,6 +87,7 @@ func walletMain() error { return err } server.Start() + server.SetWallet(wallet) // Shutdown the server if an interrupt signal is received. addInterruptHandler(server.Stop) @@ -121,49 +131,16 @@ func walletMain() error { 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() { - 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 // if the chain server connection was opened. select { case chainSvr := <-chainSvrChan: - w.Start(chainSvr) + wallet.Start(chainSvr) 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 // or an interrupt. server.WaitForShutdown() diff --git a/chain/chain.go b/chain/chain.go index 8e4c960..31a5560 100644 --- a/chain/chain.go +++ b/chain/chain.go @@ -26,8 +26,8 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcrpcclient" "github.com/btcsuite/btcutil" - "github.com/btcsuite/btcwallet/legacy/keystore" "github.com/btcsuite/btcwallet/txstore" + "github.com/btcsuite/btcwallet/waddrmgr" ) // Client represents a persistent client connection to a bitcoin RPC server @@ -38,7 +38,7 @@ type Client struct { enqueueNotification chan interface{} dequeueNotification chan interface{} - currentBlock chan *keystore.BlockStamp + currentBlock chan *waddrmgr.BlockStamp // Notification channels regarding the state of the client. These exist // 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, enqueueNotification: make(chan interface{}), dequeueNotification: make(chan interface{}), - currentBlock: make(chan *keystore.BlockStamp), + currentBlock: make(chan *waddrmgr.BlockStamp), notificationLock: new(sync.Mutex), quit: make(chan struct{}), } @@ -157,11 +157,11 @@ func (c *Client) WaitForShutdown() { type ( // BlockConnected is a notification for a newly-attached block to the // best chain. - BlockConnected keystore.BlockStamp + BlockConnected waddrmgr.BlockStamp // BlockDisconnected is a notifcation that the block described by the // 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 // address. @@ -204,7 +204,7 @@ func (c *Client) Notifications() <-chan interface{} { // BlockStamp returns the latest block notified by the client, or an error // if the client has been shut down. -func (c *Client) BlockStamp() (*keystore.BlockStamp, error) { +func (c *Client) BlockStamp() (*waddrmgr.BlockStamp, error) { select { case bs := <-c.currentBlock: return bs, nil @@ -238,11 +238,11 @@ func (c *Client) onClientConnect() { } 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) { - c.enqueueNotification <- BlockDisconnected{Hash: hash, Height: height} + c.enqueueNotification <- BlockDisconnected{Hash: *hash, Height: height} } func (c *Client) onRecvTx(tx *btcutil.Tx, block *btcws.BlockDetails) { @@ -294,7 +294,7 @@ func (c *Client) handler() { 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 // notifications, try dropping ones where a later enqueued notification @@ -329,7 +329,7 @@ out: case dequeue <- next: if n, ok := next.(BlockConnected); ok { - bs = (*keystore.BlockStamp)(&n) + bs = (*waddrmgr.BlockStamp)(&n) } notifications[0] = nil diff --git a/chainntfns.go b/chainntfns.go index c3fd24a..cbe4006 100644 --- a/chainntfns.go +++ b/chainntfns.go @@ -20,8 +20,8 @@ import ( "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcutil" "github.com/btcsuite/btcwallet/chain" - "github.com/btcsuite/btcwallet/legacy/keystore" "github.com/btcsuite/btcwallet/txstore" + "github.com/btcsuite/btcwallet/waddrmgr" ) func (w *Wallet) handleChainNotifications() { @@ -29,9 +29,9 @@ func (w *Wallet) handleChainNotifications() { var err error switch n := n.(type) { case chain.BlockConnected: - w.connectBlock(keystore.BlockStamp(n)) + w.connectBlock(waddrmgr.BlockStamp(n)) case chain.BlockDisconnected: - w.disconnectBlock(keystore.BlockStamp(n)) + w.disconnectBlock(waddrmgr.BlockStamp(n)) case chain.RecvTx: err = w.addReceivedTx(n.Tx, n.Block) case chain.RedeemingTx: @@ -53,13 +53,16 @@ func (w *Wallet) handleChainNotifications() { // connectBlock handles a chain server notification by marking a wallet // that's currently in-sync with the chain server as being synced up to // the passed block. -func (w *Wallet) connectBlock(bs keystore.BlockStamp) { +func (w *Wallet) connectBlock(bs waddrmgr.BlockStamp) { if !w.ChainSynced() { return } - w.KeyStore.SetSyncedWith(&bs) - w.KeyStore.MarkDirty() + if err := w.Manager.SetSyncedTo(&bs); err != nil { + 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.notifyBalances(bs.Height) @@ -68,22 +71,26 @@ func (w *Wallet) connectBlock(bs keystore.BlockStamp) { // disconnectBlock handles a chain server reorganize by rolling back all // block history from the reorged block for a wallet in-sync with the chain // server. -func (w *Wallet) disconnectBlock(bs keystore.BlockStamp) { +func (w *Wallet) disconnectBlock(bs waddrmgr.BlockStamp) { if !w.ChainSynced() { return } - // Disconnect the last seen block from the keystore if it - // matches the removed block. - iter := w.KeyStore.NewIterateRecentBlocks() - if iter != nil && *iter.BlockStamp().Hash == *bs.Hash { + // Disconnect the last seen block from the manager if it matches the + // removed block. + iter := w.Manager.NewIterateRecentBlocks() + if iter != nil && iter.BlockStamp().Hash == bs.Hash { if iter.Prev() { prev := iter.BlockStamp() - w.KeyStore.SetSyncedWith(&prev) + w.Manager.SetSyncedTo(&prev) } 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) @@ -103,7 +110,7 @@ func (w *Wallet) addReceivedTx(tx *btcutil.Tx, block *txstore.Block) error { activeNet.Params) insert := false for _, addr := range addrs { - _, err := w.KeyStore.Address(addr) + _, err := w.Manager.Address(addr) if err == nil { insert = true break @@ -150,7 +157,6 @@ func (w *Wallet) addRedeemingTx(tx *btcutil.Tx, block *txstore.Block) error { if _, err := txr.AddDebits(); err != nil { return err } - w.KeyStore.MarkDirty() bs, err := w.chainSvr.BlockStamp() if err == nil { diff --git a/config.go b/config.go index 2f7912a..905cf53 100644 --- a/config.go +++ b/config.go @@ -26,19 +26,22 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcwallet/legacy/keystore" flags "github.com/btcsuite/go-flags" ) const ( - defaultCAFilename = "btcd.cert" - defaultConfigFilename = "btcwallet.conf" - defaultBtcNet = wire.TestNet3 - defaultLogLevel = "info" - defaultLogDirname = "logs" - defaultLogFilename = "btcwallet.log" - defaultDisallowFree = false - defaultRPCMaxClients = 10 - defaultRPCMaxWebsockets = 25 + defaultCAFilename = "btcd.cert" + defaultConfigFilename = "btcwallet.conf" + defaultBtcNet = wire.TestNet3 + defaultLogLevel = "info" + defaultLogDirname = "logs" + defaultLogFilename = "btcwallet.log" + defaultDisallowFree = false + defaultRPCMaxClients = 10 + defaultRPCMaxWebsockets = 25 + walletDbName = "wallet.db" + walletDbWatchingOnlyName = "wowallet.db" ) var ( @@ -54,6 +57,7 @@ var ( type config struct { 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"` 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}"` @@ -65,6 +69,7 @@ type config struct { 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"` 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"` RPCKey string `long:"rpckey" description:"File containing the certificate key"` RPCMaxClients int64 `long:"rpcmaxclients" description:"Max number of RPC clients for standard connections"` @@ -242,6 +247,7 @@ func loadConfig() (*config, []string, error) { ConfigFile: defaultConfigFile, DataDir: defaultDataDir, LogDir: defaultLogDir, + WalletPass: defaultPubPassphrase, RPCKey: defaultRPCKeyFile, RPCCert: defaultRPCCertFile, DisallowFree: defaultDisallowFree, @@ -360,6 +366,47 @@ func loadConfig() (*config, []string, error) { 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 == "" { cfg.RPCConnect = activeNet.connect } diff --git a/createtx.go b/createtx.go index e3f9327..b62cc1f 100644 --- a/createtx.go +++ b/createtx.go @@ -27,8 +27,8 @@ import ( "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" - "github.com/btcsuite/btcwallet/legacy/keystore" "github.com/btcsuite/btcwallet/txstore" + "github.com/btcsuite/btcwallet/waddrmgr" ) 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. func (w *Wallet) txToPairs(pairs map[string]btcutil.Amount, minconf int) (*CreatedTx, error) { - // Key store must be unlocked to compose transaction. Grab the - // unlock if possible (to prevent future unlocks), or return the - // error if the keystore is already locked. + // Address manager must be unlocked to compose transaction. Grab + // the unlock if possible (to prevent future unlocks), or return the + // error if already locked. heldUnlock, err := w.HoldUnlock() if err != nil { return nil, err @@ -150,7 +150,7 @@ func (w *Wallet) txToPairs(pairs map[string]btcutil.Amount, minconf int) (*Creat 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) @@ -161,10 +161,10 @@ func (w *Wallet) txToPairs(pairs map[string]btcutil.Amount, minconf int) (*Creat func createTx( eligible []txstore.Credit, outputs map[string]btcutil.Amount, - bs *keystore.BlockStamp, + bs *waddrmgr.BlockStamp, feeIncrement btcutil.Amount, - keys *keystore.Store, - changeAddress func(*keystore.BlockStamp) (btcutil.Address, error)) ( + mgr *waddrmgr.Manager, + changeAddress func(*waddrmgr.BlockStamp) (btcutil.Address, error)) ( *CreatedTx, error) { 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 } @@ -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 // transaction output. It will also mark the KeyStore as dirty and // tells chainSvr to watch that address. -func (w *Wallet) changeAddress(bs *keystore.BlockStamp) (btcutil.Address, error) { - changeAddr, err := w.KeyStore.ChangeAddress(bs) +func (w *Wallet) changeAddress(bs *waddrmgr.BlockStamp) (btcutil.Address, error) { + changeAddrs, err := w.Manager.NextInternalAddresses(0, 1) if err != nil { 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}) if err != nil { 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 } -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() if err != nil { 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. // It must be called every time a msgtx is changed. // 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) { return fmt.Errorf( "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 } - ai, err := store.Address(apkh) + ai, err := mgr.Address(apkh) if err != nil { return fmt.Errorf("cannot get address info: %v", err) } - pka := ai.(keystore.PubKeyAddress) + pka := ai.(waddrmgr.ManagedPubKeyAddress) privkey, err := pka.PrivKey() if err != nil { return fmt.Errorf("cannot get private key: %v", err) } - sigscript, err := txscript.SignatureScript( - msgtx, i, output.TxOut().PkScript, txscript.SigHashAll, privkey, ai.Compressed()) + sigscript, err := txscript.SignatureScript(msgtx, i, + output.TxOut().PkScript, txscript.SigHashAll, privkey, + ai.Compressed()) if err != nil { return fmt.Errorf("cannot create sigscript: %s", err) } diff --git a/createtx_test.go b/createtx_test.go index 4becb72..c659c42 100644 --- a/createtx_test.go +++ b/createtx_test.go @@ -2,6 +2,8 @@ package main import ( "encoding/hex" + "os" + "path/filepath" "reflect" "sort" "testing" @@ -9,8 +11,11 @@ import ( "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" - "github.com/btcsuite/btcwallet/keystore" + "github.com/btcsuite/btcutil/hdkeychain" "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. @@ -40,6 +45,14 @@ var ( 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) { msgtx := wire.NewMsgTx() pairs := map[string]btcutil.Amount{outAddr1: 10, outAddr2: 1} @@ -58,10 +71,10 @@ func Test_addOutputs(t *testing.T) { func TestCreateTx(t *testing.T) { cfg = &config{DisallowFree: false} - bs := &keystore.BlockStamp{Height: 11111} - keys := newKeyStore(t, txInfo.privKeys, bs) + bs := &waddrmgr.BlockStamp{Height: 11111} + mgr := newManager(t, txInfo.privKeys, bs) 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 } @@ -69,7 +82,7 @@ func TestCreateTx(t *testing.T) { eligible := eligibleInputsFromTx(t, txInfo.hex, []uint32{1, 2, 3, 4, 5}) // Now create a new TX sending 25e6 satoshis to the following addresses: 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 { t.Fatal(err) } @@ -110,9 +123,9 @@ func TestCreateTxInsufficientFundsError(t *testing.T) { cfg = &config{DisallowFree: false} outputs := map[string]btcutil.Amount{outAddr1: 10, outAddr2: 1e9} eligible := eligibleInputsFromTx(t, txInfo.hex, []uint32{1}) - bs := &keystore.BlockStamp{Height: 11111} + bs := &waddrmgr.BlockStamp{Height: 11111} 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 } @@ -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. -func newKeyStore(t *testing.T, privKeys []string, bs *keystore.BlockStamp) *keystore.Store { - passphrase := []byte{0, 1} - keys, err := keystore.New("/tmp/keys.bin", "Default acccount", passphrase, - activeNet.Params, bs) +// newManager creates a new waddrmgr and imports the given privKey into it. +func newManager(t *testing.T, privKeys []string, bs *waddrmgr.BlockStamp) *waddrmgr.Manager { + dbPath := filepath.Join(os.TempDir(), "wallet.bin") + os.Remove(dbPath) + db, err := walletdb.Create("bdb", dbPath) if err != nil { 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 { wif, err := btcutil.DecodeWIF(key) if err != nil { t.Fatal(err) } - if err = keys.Unlock(passphrase); err != nil { + if err = mgr.Unlock(privPassphrase); err != nil { t.Fatal(err) } - _, err = keys.ImportPrivateKey(wif, bs) + _, err = mgr.ImportPrivateKey(wif, bs) if err != nil { t.Fatal(err) } } - return keys + return mgr } // eligibleInputsFromTx decodes the given txHex and returns the outputs with diff --git a/rescan.go b/rescan.go index cfcfd00..c3213f1 100644 --- a/rescan.go +++ b/rescan.go @@ -20,7 +20,7 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" "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 @@ -47,7 +47,7 @@ type RescanJob struct { InitialSync bool Addrs []btcutil.Address OutPoints []*wire.OutPoint - BlockStamp keystore.BlockStamp + BlockStamp waddrmgr.BlockStamp err chan error } @@ -57,7 +57,7 @@ type rescanBatch struct { initialSync bool addrs []btcutil.Address outpoints []*wire.OutPoint - bs keystore.BlockStamp + bs waddrmgr.BlockStamp errChans []chan error } @@ -172,9 +172,8 @@ out: w.wg.Done() } -// rescanProgressHandler handles notifications for paritally and fully completed -// rescans by marking each rescanned address as partially or fully synced and -// writing the keystore back to disk. +// rescanProgressHandler handles notifications for partially and fully completed +// rescans by marking each rescanned address as partially or fully synced. func (w *Wallet) rescanProgressHandler() { out: for { @@ -187,21 +186,14 @@ out: log.Infof("Rescanned through block %v (height %d)", n.Hash, n.Height) - // TODO(jrick): save partial syncs should also include - // the block hash. - for _, addr := range msg.Addresses { - err := w.KeyStore.SetSyncStatus(addr, - keystore.PartialSync(n.Height)) - if err != nil { - log.Errorf("Error marking address %v "+ - "partially synced: %v", addr, err) - } + bs := waddrmgr.BlockStamp{ + Hash: *n.Hash, + Height: n.Height, } - w.KeyStore.MarkDirty() - err := w.KeyStore.WriteIfDirty() - if err != nil { - log.Errorf("Could not write partial rescan "+ - "progress to keystore: %v", err) + if err := w.Manager.SetSyncedTo(&bs); err != nil { + log.Errorf("Failed to update address manager "+ + "sync state for hash %v (height %d): %v", + n.Hash, n.Height, err) } case msg := <-w.rescanFinished: @@ -211,11 +203,17 @@ out: if msg.WasInitialSync { w.ResendUnminedTxs() - bs := keystore.BlockStamp{ - Hash: n.Hash, + bs := waddrmgr.BlockStamp{ + Hash: *n.Hash, 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) // Mark wallet as synced to chain so connected @@ -227,21 +225,6 @@ out: "%s, height %d)", len(addrs), noun, n.Hash, 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: break out } @@ -260,7 +243,7 @@ func (w *Wallet) rescanRPCHandler() { log.Infof("Started rescan from block %v (height %d) for %d %s", 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) if err != nil { log.Errorf("Rescan for %d %s failed: %v", numAddrs, @@ -271,34 +254,24 @@ func (w *Wallet) rescanRPCHandler() { w.wg.Done() } -// RescanActiveAddresses begins a rescan for all active addresses of a -// wallet. This is intended to be used to sync a wallet back up to the -// current best block in the main chain, and is considered an intial sync -// rescan. -func (w *Wallet) RescanActiveAddresses() (err error) { - // 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 { - return - } +// RescanActiveAddresses begins a rescan for all active addresses of a wallet. +// This is intended to be used to sync a wallet back up to the current best +// block in the main chain, and is considered an initial sync rescan. +func (w *Wallet) RescanActiveAddresses() error { + addrs, err := w.Manager.AllActiveAddresses() + if err != nil { + return err } - actives := w.KeyStore.SortedActiveAddresses() - addrs := make([]btcutil.Address, len(actives)) - for i, addr := range actives { - addrs[i] = addr.Address() + // in case there are no addresses, we can skip queuing the rescan job + if len(addrs) == 0 { + close(w.chainSynced) + return nil } unspents, err := w.TxStore.UnspentOutputs() if err != nil { - return + return err } outpoints := make([]*wire.OutPoint, len(unspents)) for i, output := range unspents { @@ -309,7 +282,7 @@ func (w *Wallet) RescanActiveAddresses() (err error) { InitialSync: true, Addrs: addrs, OutPoints: outpoints, - BlockStamp: keystore.BlockStamp{Hash: hash, Height: height}, + BlockStamp: w.Manager.SyncedTo(), } // Submit merged job and block until rescan completes. diff --git a/rpcserver.go b/rpcserver.go index f6e92fd..f6eced7 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -45,8 +45,8 @@ import ( "github.com/btcsuite/btcrpcclient" "github.com/btcsuite/btcutil" "github.com/btcsuite/btcwallet/chain" - "github.com/btcsuite/btcwallet/legacy/keystore" "github.com/btcsuite/btcwallet/txstore" + "github.com/btcsuite/btcwallet/waddrmgr" "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 // IPv4 and IPv6 slices and returns them. This allows easy creation of the // 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 // created. - connectedBlocks <-chan keystore.BlockStamp - disconnectedBlocks <-chan keystore.BlockStamp + connectedBlocks <-chan waddrmgr.BlockStamp + disconnectedBlocks <-chan waddrmgr.BlockStamp newCredits <-chan txstore.Credit newDebits <-chan txstore.Debits minedCredits <-chan txstore.Credit minedDebits <-chan txstore.Debits - keystoreLocked <-chan bool + managerLocked <-chan bool confirmedBalance <-chan btcutil.Amount unconfirmedBalance <-chan btcutil.Amount 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. // // NOTE: These handlers do not handle special cases, such as the authenticate -// and createencryptedwallet methods. Each of these must be checked -// beforehand (the method is already known) and handled accordingly. +// method. Each of these must be checked beforehand (the method is already +// known) and handled accordingly. func (s *rpcServer) HandlerClosure(method string) requestHandlerClosure { s.handlerLock.Lock() defer s.handlerLock.Unlock() @@ -800,19 +825,6 @@ out: } 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": s.Stop() 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 - // are handled for the authenticate, createencryptedwallet, and stop - // request methods. + // are handled for the authenticate and stop request methods. var resp btcjson.Reply switch raw.Method { case "authenticate": // Drop it. return - case "createencryptedwallet": - result, err := s.handleCreateEncryptedWallet(rpcRequest) - resp = makeResponse(raw.ID, result, err) case "stop": s.Stop() resp = makeResponse(raw.ID, "btcwallet stopping.", nil) @@ -1024,13 +1032,13 @@ type ( notificationCmds(w *Wallet) []btcjson.Cmd } - blockConnected keystore.BlockStamp - blockDisconnected keystore.BlockStamp + blockConnected waddrmgr.BlockStamp + blockDisconnected waddrmgr.BlockStamp txCredit txstore.Credit txDebit txstore.Debits - keystoreLocked bool + managerLocked bool confirmedBalance btcutil.Amount unconfirmedBalance btcutil.Amount @@ -1085,8 +1093,8 @@ func (d txDebit) notificationCmds(w *Wallet) []btcjson.Cmd { return ns } -func (kl keystoreLocked) notificationCmds(w *Wallet) []btcjson.Cmd { - n := btcws.NewWalletLockStateNtfn("", bool(kl)) +func (l managerLocked) notificationCmds(w *Wallet) []btcjson.Cmd { + n := btcws.NewWalletLockStateNtfn("", bool(l)) return []btcjson.Cmd{n} } @@ -1123,8 +1131,8 @@ out: s.enqueueNotification <- txCredit(n) case n := <-s.minedDebits: s.enqueueNotification <- txDebit(n) - case n := <-s.keystoreLocked: - s.enqueueNotification <- keystoreLocked(n) + case n := <-s.managerLocked: + s.enqueueNotification <- managerLocked(n) case n := <-s.confirmedBalance: s.enqueueNotification <- confirmedBalance(n) case n := <-s.unconfirmedBalance: @@ -1173,9 +1181,9 @@ out: "debit notifications: %v", err) continue } - keystoreLocked, err := s.wallet.ListenKeystoreLockStatus() + managerLocked, err := s.wallet.ListenLockStatus() if err != nil { - log.Errorf("Could not register for keystore "+ + log.Errorf("Could not register for manager "+ "lock state changes: %v", err) continue } @@ -1197,7 +1205,7 @@ out: s.newDebits = newDebits s.minedCredits = minedCredits s.minedDebits = minedDebits - s.keystoreLocked = keystoreLocked + s.managerLocked = managerLocked s.confirmedBalance = confirmedBalance s.unconfirmedBalance = unconfirmedBalance @@ -1248,7 +1256,7 @@ func (s *rpcServer) drainNotifications() { // notificationQueue manages an infinitly-growing queue of notifications that // 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. func (s *rpcServer) notificationQueue() { var q []wsClientNotification @@ -1403,7 +1411,6 @@ var rpcHandlers = map[string]requestHandler{ "getunconfirmedbalance": GetUnconfirmedBalance, "listaddresstransactions": ListAddressTransactions, "listalltransactions": ListAllTransactions, - "recoveraddresses": RecoverAddresses, "walletislocked": WalletIsLocked, } @@ -1549,12 +1556,12 @@ func makeMultiSigScript(w *Wallet, keys []string, nRequired int) ([]byte, error) case *btcutil.AddressPubKey: keysesPrecious[i] = addr case *btcutil.AddressPubKeyHash: - ainfo, err := w.KeyStore.Address(addr) + ainfo, err := w.Manager.Address(addr) if err != nil { return nil, err } - apkinfo := ainfo.(keystore.PubKeyAddress) + apkinfo := ainfo.(waddrmgr.ManagedPubKeyAddress) // This will be an addresspubkey 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? - address, err := w.KeyStore.ImportScript(script, - &keystore.BlockStamp{}) + bs := &waddrmgr.BlockStamp{ + Hash: *activeNet.Params.GenesisHash, + Height: 0, + } + + addr, err := w.Manager.ImportScript(script, bs) if err != nil { return nil, err } - // Write wallet with imported multisig address to disk. - w.KeyStore.MarkDirty() - err = w.KeyStore.WriteIfDirty() - if err != nil { - return nil, fmt.Errorf("account write failed: %v", err) - } - - return address.EncodeAddress(), nil + return addr.Address().EncodeAddress(), nil } // 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) - if err == keystore.ErrLocked { + if isManagerLockedError(err) { // Address was found, but the private key isn't // accessible. 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. func DumpWallet(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { keys, err := w.DumpPrivKeys() - if err == keystore.ErrLocked { + if isManagerLockedError(err) { return nil, btcjson.ErrWalletUnlockNeeded } + return keys, err } @@ -1671,12 +1676,7 @@ func ExportWatchingWallet(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) ( return nil, err } - wa, err := w.ExportWatchingWallet() - if err != nil { - return nil, err - } - - return wa.exportBase64() + return w.ExportWatchingWallet() } // 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 w.SortedActivePaymentAddresses(), nil + return w.SortedActivePaymentAddresses() } // 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 } - 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) // Keypool times are not tracked. set to current time. 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. - _, err = w.KeyStore.Address(addr) + _, err = w.Manager.Address(addr) if err != nil { 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. - _, err = w.ImportPrivateKey(wif, &keystore.BlockStamp{}, cmd.Rescan) - switch err { - case keystore.ErrDuplicate: + _, err = w.ImportPrivateKey(wif, nil, cmd.Rescan) + switch { + case isManagerDuplicateError(err): // Do not return duplicate key errors to the client. return nil, nil - case keystore.ErrLocked: + case isManagerLockedError(err): 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 @@ -2124,7 +2125,11 @@ func ListReceivedByAddress(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) if cmd.IncludeEmpty { // Create an AddrData entry for each active address in the account. // 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. allAddrData[address] = AddrData{} } @@ -2352,14 +2357,14 @@ func sendPairs(w *Wallet, chainSvr *chain.Client, cmd btcjson.Cmd, // was not successful. createdTx, err := w.CreateSimpleTx(amounts, minconf) if err != nil { - switch err { - case ErrNonPositiveAmount: + switch { + case err == ErrNonPositiveAmount: return nil, ErrNeedPositiveAmount - case keystore.ErrLocked: + case isManagerLockedError(err): return nil, btcjson.ErrWalletUnlockNeeded - default: - return nil, err } + + return nil, err } // Add to transaction store. @@ -2492,17 +2497,16 @@ func SignMessage(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface return nil, ParseError{err} } - ainfo, err := w.KeyStore.Address(addr) + ainfo, err := w.Manager.Address(addr) if err != nil { return nil, btcjson.ErrInvalidAddressOrKey } - pka := ainfo.(keystore.PubKeyAddress) - tmp, err := pka.PrivKey() + pka := ainfo.(waddrmgr.ManagedPubKeyAddress) + privKey, err := pka.PrivKey() if err != nil { return nil, err } - privKey := (*btcec.PrivateKey)(tmp) fullmsg := "Bitcoin Signed Message:\n" + cmd.Message 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 } -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 // SignRawTransaction. type pendingTx struct { @@ -2774,12 +2713,12 @@ func SignRawTransaction(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (in } return wif.PrivKey, wif.CompressPubKey, nil } - address, err := w.KeyStore.Address(addr) + address, err := w.Manager.Address(addr) if err != nil { return nil, false, err } - pka, ok := address.(keystore.PubKeyAddress) + pka, ok := address.(waddrmgr.ManagedPubKeyAddress) if !ok { return nil, false, errors.New("address is not " + "a pubkey address") @@ -2805,20 +2744,17 @@ func SignRawTransaction(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (in } return script, nil } - address, err := w.KeyStore.Address(addr) + address, err := w.Manager.Address(addr) if err != nil { return nil, err } - sa, ok := address.(keystore.ScriptAddress) + sa, ok := address.(waddrmgr.ManagedScriptAddress) if !ok { return nil, errors.New("address is not a script" + " address") } - // TODO(oga) we could possible speed things up further - // by returning the addresses, class and nrequired here - // thus avoiding recomputing them. - return sa.Script(), nil + return sa.Script() }) // 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.IsValid = true - ainfo, err := w.KeyStore.Address(addr) - if err == nil { - result.IsMine = true - result.Account = "" + ainfo, err := w.Manager.Address(addr) + 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 + } - if pka, ok := ainfo.(keystore.PubKeyAddress); ok { - result.IsCompressed = pka.Compressed() - result.PubKey = pka.ExportPubKey() + // The address lookup was successful which means there is further + // information about it available and it is "mine". + result.IsMine = true + result.Account = "" - } else if sa, ok := ainfo.(keystore.ScriptAddress); ok { - result.IsScript = true - addresses := sa.Addresses() - addrStrings := make([]string, len(addresses)) - for i, a := range addresses { - addrStrings[i] = a.EncodeAddress() - } - result.Addresses = addrStrings - result.Hex = hex.EncodeToString(sa.Script()) + switch ma := ainfo.(type) { + case waddrmgr.ManagedPubKeyAddress: + result.IsCompressed = ma.Compressed() + result.PubKey = ma.ExportPubKey() - class := sa.ScriptClass() - // script type - result.Script = class.String() - if class == txscript.MultiSigTy { - result.SigsRequired = int32(sa.RequiredSigs()) - } + case waddrmgr.ManagedScriptAddress: + result.IsScript = true + + // The script is only available if the manager is unlocked, so + // 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() + } + result.Addresses = addrStrings + + // Multi-signature scripts also provide the number of required + // signatures. + result.Script = class.String() + if class == txscript.MultiSigTy { + result.SigsRequired = int32(reqSigs) } } @@ -2992,7 +2957,7 @@ func WalletPassphraseChange(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) err := w.ChangePassphrase([]byte(cmd.OldPassphrase), []byte(cmd.NewPassphrase)) - if err == keystore.ErrWrongPassphrase { + if isManagerWrongPassphraseError(err) { return nil, btcjson.ErrWalletPassphraseIncorrect } return nil, err diff --git a/snacl/snacl.go b/snacl/snacl.go index 8d72f34..22da16f 100644 --- a/snacl/snacl.go +++ b/snacl/snacl.go @@ -6,6 +6,7 @@ import ( "encoding/binary" "errors" "io" + "runtime/debug" "golang.org/x/crypto/nacl/secretbox" "golang.org/x/crypto/scrypt" @@ -122,6 +123,14 @@ func (sk *SecretKey) deriveKey(password *[]byte) error { copy(sk.Key[:], 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 } diff --git a/waddrmgr/common_test.go b/waddrmgr/common_test.go index 1139eae..239504c 100644 --- a/waddrmgr/common_test.go +++ b/waddrmgr/common_test.go @@ -50,6 +50,9 @@ var ( ScryptR: 8, 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 @@ -88,7 +91,7 @@ func createDbNamespace(dbPath string) (walletdb.DB, walletdb.Namespace, error) { return nil, nil, err } - namespace, err := db.Namespace([]byte("waddrmgr")) + namespace, err := db.Namespace(waddrmgrNamespaceKey) if err != nil { db.Close() return nil, nil, err @@ -105,7 +108,7 @@ func openDbNamespace(dbPath string) (walletdb.DB, walletdb.Namespace, error) { return nil, nil, err } - namespace, err := db.Namespace([]byte("waddrmgr")) + namespace, err := db.Namespace(waddrmgrNamespaceKey) if err != nil { db.Close() return nil, nil, err diff --git a/waddrmgr/manager.go b/waddrmgr/manager.go index d1e9d8f..eda637f 100644 --- a/waddrmgr/manager.go +++ b/waddrmgr/manager.go @@ -145,8 +145,8 @@ func defaultNewSecretKey(passphrase *[]byte, config *Options) (*snacl.SecretKey, // paths. var newSecretKey = defaultNewSecretKey -// EncryptorDecryptor provides an abstraction on top of snacl.CryptoKey so that our -// tests can use dependency injection to force the behaviour they need. +// EncryptorDecryptor provides an abstraction on top of snacl.CryptoKey so that +// our tests can use dependency injection to force the behaviour they need. type EncryptorDecryptor interface { Encrypt(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 // no effect. -func (m *Manager) ConvertToWatchingOnly(pubPassphrase []byte) error { +func (m *Manager) ConvertToWatchingOnly() error { m.mtx.Lock() defer m.mtx.Unlock() diff --git a/waddrmgr/manager_test.go b/waddrmgr/manager_test.go index d52d7ec..e954384 100644 --- a/waddrmgr/manager_test.go +++ b/waddrmgr/manager_test.go @@ -1168,7 +1168,7 @@ func testWatchingOnly(tc *testContext) bool { tc.t.Errorf("%v", err) return false } - if err := mgr.ConvertToWatchingOnly(pubPassphrase); err != nil { + if err := mgr.ConvertToWatchingOnly(); err != nil { tc.t.Errorf("%v", err) return false } diff --git a/wallet.go b/wallet.go index 723da46..f21bf85 100644 --- a/wallet.go +++ b/wallet.go @@ -22,7 +22,10 @@ import ( "encoding/hex" "errors" "fmt" + "io/ioutil" + "os" "path/filepath" + "sort" "sync" "time" @@ -32,8 +35,9 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" "github.com/btcsuite/btcwallet/chain" - "github.com/btcsuite/btcwallet/legacy/keystore" "github.com/btcsuite/btcwallet/txstore" + "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/btcwallet/walletdb" ) // ErrNotSynced describes an error where an operation cannot complete @@ -41,9 +45,22 @@ import ( // the remote 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 // files. -func networkDir(chainParams *chaincfg.Params) string { +func networkDir(dataDir string, chainParams *chaincfg.Params) string { netname := chainParams.Name // For now, we must always name the testnet data directory as "testnet" @@ -55,7 +72,7 @@ func networkDir(chainParams *chaincfg.Params) string { netname = "testnet" } - return filepath.Join(cfg.DataDir, netname) + return filepath.Join(dataDir, netname) } // Wallet is a structure containing all the components for a @@ -63,8 +80,9 @@ func networkDir(chainParams *chaincfg.Params) string { // addresses and keys), type Wallet struct { // Data stores - KeyStore *keystore.Store - TxStore *txstore.Store + db walletdb.DB + Manager *waddrmgr.Manager + TxStore *txstore.Store chainSvr *chain.Client chainSvrLock sync.Locker @@ -85,7 +103,7 @@ type Wallet struct { // Channel for transaction creation requests. createTxRequests chan createTxRequest - // Channels for the keystore locker. + // Channels for the manager locker. unlockRequests chan unlockRequest lockRequests chan struct{} holdUnlockRequests chan chan HeldUnlock @@ -95,8 +113,8 @@ type Wallet struct { // Notification channels so other components can listen in on wallet // activity. These are initialized as nil, and must be created by // calling one of the Listen* methods. - connectedBlocks chan keystore.BlockStamp - disconnectedBlocks chan keystore.BlockStamp + connectedBlocks chan waddrmgr.BlockStamp + disconnectedBlocks chan waddrmgr.BlockStamp lockStateChanges chan bool // true when locked confirmedBalance chan btcutil.Amount unconfirmedBalance chan btcutil.Amount @@ -106,11 +124,11 @@ type Wallet struct { quit chan struct{} } -// newWallet creates a new Wallet structure with the provided key and -// transaction stores. -func newWallet(keys *keystore.Store, txs *txstore.Store) *Wallet { +// newWallet creates a new Wallet structure with the provided address manager +// and transaction store. +func newWallet(mgr *waddrmgr.Manager, txs *txstore.Store) *Wallet { return &Wallet{ - KeyStore: keys, + Manager: mgr, TxStore: txs, chainSvrLock: new(sync.Mutex), chainSynced: make(chan struct{}), @@ -158,14 +176,14 @@ func (w *Wallet) updateNotificationLock() { // methods will block. // // 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() defer w.notificationLock.Unlock() if w.connectedBlocks != nil { return nil, ErrDuplicateListen } - w.connectedBlocks = make(chan keystore.BlockStamp) + w.connectedBlocks = make(chan waddrmgr.BlockStamp) w.updateNotificationLock() return w.connectedBlocks, nil } @@ -175,25 +193,25 @@ func (w *Wallet) ListenConnectedBlocks() (<-chan keystore.BlockStamp, error) { // block. // // 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() defer w.notificationLock.Unlock() if w.disconnectedBlocks != nil { return nil, ErrDuplicateListen } - w.disconnectedBlocks = make(chan keystore.BlockStamp) + w.disconnectedBlocks = make(chan waddrmgr.BlockStamp) w.updateNotificationLock() return w.disconnectedBlocks, nil } -// ListenKeystoreLockStatus returns a channel that passes the current lock state -// of the wallet keystore anytime the keystore is locked or unlocked. The value -// is true for locked, and false for unlocked. The channel must be read, or -// other wallet methods will block. +// ListenLockStatus returns a channel that passes the current lock state +// of the wallet whenever the lock state is changed. The value is true for +// locked, and false for unlocked. The channel must be read, or other wallet +// methods will block. // // 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() defer w.notificationLock.Unlock() @@ -239,7 +257,7 @@ func (w *Wallet) ListenUnconfirmedBalance() (<-chan btcutil.Amount, error) { return w.unconfirmedBalance, nil } -func (w *Wallet) notifyConnectedBlock(block keystore.BlockStamp) { +func (w *Wallet) notifyConnectedBlock(block waddrmgr.BlockStamp) { w.notificationLock.Lock() if w.connectedBlocks != nil { w.connectedBlocks <- block @@ -247,7 +265,7 @@ func (w *Wallet) notifyConnectedBlock(block keystore.BlockStamp) { w.notificationLock.Unlock() } -func (w *Wallet) notifyDisconnectedBlock(block keystore.BlockStamp) { +func (w *Wallet) notifyDisconnectedBlock(block waddrmgr.BlockStamp) { w.notificationLock.Lock() if w.disconnectedBlocks != nil { w.disconnectedBlocks <- block @@ -279,70 +297,6 @@ func (w *Wallet) notifyUnconfirmedBalance(bal btcutil.Amount) { 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. func (w *Wallet) Start(chainServer *chain.Client) { select { @@ -361,7 +315,7 @@ func (w *Wallet) Start(chainServer *chain.Client) { go w.diskWriter() go w.handleChainNotifications() go w.txCreator() - go w.keystoreLocker() + go w.walletLocker() go w.rescanBatchHandler() go w.rescanProgressHandler() go w.rescanRPCHandler() @@ -429,7 +383,7 @@ func (w *Wallet) WaitForChainSync() { // SyncedChainTip returns the hash and height of the block of the most // recently seen block in the main chain. It returns errors if the // 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 { case <-w.chainSynced: 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. // If so, rollback and rescan to catch up. - iter := w.KeyStore.NewIterateRecentBlocks() + iter := w.Manager.NewIterateRecentBlocks() for cont := iter != nil; cont; cont = iter.Prev() { bs := iter.BlockStamp() log.Debugf("Checking for previous saved block with height %v hash %v", bs.Height, bs.Hash) - if _, err := w.chainSvr.GetBlock(bs.Hash); err != nil { + if _, err := w.chainSvr.GetBlock(&bs.Hash); err != nil { continue } @@ -468,7 +422,7 @@ func (w *Wallet) syncWithChain() (err error) { // returns true), then rollback the next and all child blocks. if iter.Next() { bs := iter.BlockStamp() - w.KeyStore.SetSyncedWith(&bs) + w.Manager.SetSyncedTo(&bs) err = w.TxStore.Rollback(bs.Height) if err != nil { return @@ -556,15 +510,15 @@ type ( HeldUnlock chan struct{} ) -// keystoreLocker manages the locked/unlocked state of a wallet. -func (w *Wallet) keystoreLocker() { +// walletLocker manages the locked/unlocked state of a wallet. +func (w *Wallet) walletLocker() { var timeout <-chan time.Time holdChan := make(HeldUnlock) out: for { select { case req := <-w.unlockRequests: - err := w.KeyStore.Unlock(req.passphrase) + err := w.Manager.Unlock(req.passphrase) if err != nil { req.err <- err continue @@ -579,23 +533,12 @@ out: continue case req := <-w.changePassphrase: - // Changing the passphrase requires an unlocked - // 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) - } + err := w.Manager.ChangePassphrase(req.old, req.new, true) req.err <- err continue case req := <-w.holdUnlockRequests: - if w.KeyStore.IsLocked() { + if w.Manager.IsLocked() { close(req) continue } @@ -615,7 +558,7 @@ out: continue } - case w.lockState <- w.KeyStore.IsLocked(): + case w.lockState <- w.Manager.IsLocked(): continue case <-w.quit: @@ -626,19 +569,23 @@ out: } // Select statement fell through by an explicit lock or the - // timer expiring. Lock the keystores here. + // timer expiring. Lock the manager here. timeout = nil - if err := w.KeyStore.Lock(); err != nil { + err := w.Manager.Lock() + if err != nil { log.Errorf("Could not lock wallet: %v", err) + } else { + w.notifyLockStateChange(true) } - w.notifyLockStateChange(true) } w.wg.Done() } -// Unlock unlocks the wallet's keystore and locks the wallet again after -// timeout has expired. If the wallet is already unlocked and the new -// passphrase is correct, the current timeout is replaced with the new one. +// Unlock unlocks the wallet's address manager and relocks it after timeout has +// expired. If the wallet is already unlocked and the new passphrase is +// 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 { err := make(chan error, 1) w.unlockRequests <- unlockRequest{ @@ -649,12 +596,12 @@ func (w *Wallet) Unlock(passphrase []byte, timeout time.Duration) error { return <-err } -// Lock locks the wallet's keystore. +// Lock locks the wallet's address manager. func (w *Wallet) Lock() { 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 { return <-w.lockState } @@ -670,7 +617,12 @@ func (w *Wallet) HoldUnlock() (HeldUnlock, error) { w.holdUnlockRequests <- req hl, ok := <-req 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 } @@ -683,8 +635,9 @@ func (c HeldUnlock) Release() { } // ChangePassphrase attempts to change the passphrase for a wallet from old -// to new. Changing the passphrase is synchronized with all other keystore -// locking and unlocking, and will result in a locked wallet on success. +// to new. Changing the passphrase is synchronized with all other address +// 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 { err := make(chan error, 1) w.changePassphrase <- changePassphraseRequest{ @@ -695,8 +648,8 @@ func (w *Wallet) ChangePassphrase(old, new []byte) error { return <-err } -// diskWriter periodically (every 10 seconds) writes out the key and transaction -// stores to disk if they are marked dirty. On shutdown, +// diskWriter periodically (every 10 seconds) writes out the transaction store +// to disk if it is marked dirty. func (w *Wallet) diskWriter() { ticker := time.NewTicker(10 * time.Second) var wg sync.WaitGroup @@ -709,17 +662,9 @@ func (w *Wallet) diskWriter() { done = true } - log.Trace("Writing wallet files") + log.Trace("Writing txstore") - wg.Add(2) - go func() { - err := w.KeyStore.WriteIfDirty() - if err != nil { - log.Errorf("Cannot write keystore: %v", - err) - } - wg.Done() - }() + wg.Add(1) go func() { err := w.TxStore.WriteIfDirty() if err != nil { @@ -741,7 +686,7 @@ func (w *Wallet) diskWriter() { // 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 // 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 // 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 @@ -754,7 +699,7 @@ func (w *Wallet) AddressUsed(addr btcutil.Address) bool { // range below does nothing. _, addrs, _, _ := c.Addresses(activeNet.Params) for _, a := range addrs { - if addr.String() == a.String() { + if addr.Address().String() == a.String() { 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 // chained address is returned. 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. if w.AddressUsed(addr) { return w.NewAddress() } - return addr, nil + return addr.Address(), nil } // 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, - w.KeyStore.Net()) + w.Manager.ChainParams()) if err != nil { 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. for i := len(records) - 1; i >= from && i >= lastLookupIdx; i-- { jsonResults, err := records[i].ToJSON("", bs.Height, - w.KeyStore.Net()) + w.Manager.ChainParams()) if err != nil { return nil, err } @@ -886,7 +834,7 @@ func (w *Wallet) ListAddressTransactions(pkHashes map[string]struct{}) ( continue } jsonResult, err := c.ToJSON("", bs.Height, - w.KeyStore.Net()) + w.Manager.ChainParams()) if err != nil { return nil, err } @@ -914,7 +862,7 @@ func (w *Wallet) ListAllTransactions() ([]btcjson.ListTransactionsResult, error) records := w.TxStore.Records() for i := len(records) - 1; i >= 0; i-- { jsonResults, err := records[i].ToJSON("", bs.Height, - w.KeyStore.Net()) + w.Manager.ChainParams()) if err != nil { 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 // private keys in a wallet. func (w *Wallet) DumpPrivKeys() ([]string, error) { - // Iterate over each active address, appending the private - // key to privkeys. - privkeys := []string{} - for _, info := range w.KeyStore.ActiveAddresses() { + addrs, err := w.Manager.AllActiveAddresses() + if err != nil { + return nil, err + } + + // 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. - pka, ok := info.(keystore.PubKeyAddress) + pka, ok := ma.(waddrmgr.ManagedPubKeyAddress) if !ok { continue } + wif, err := pka.ExportPrivKey() if err != nil { // It would be nice to zero out the array here. However, @@ -1022,12 +981,12 @@ func (w *Wallet) DumpPrivKeys() ([]string, error) { // single wallet address. func (w *Wallet) DumpWIFPrivateKey(addr btcutil.Address) (string, error) { // Get private key from wallet if it exists. - address, err := w.KeyStore.Address(addr) + address, err := w.Manager.Address(addr) if err != nil { return "", err } - pka, ok := address.(keystore.PubKeyAddress) + pka, ok := address.(waddrmgr.ManagedPubKeyAddress) if !ok { 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 // 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) { - // Attempt to import private key into wallet. - addr, err := w.KeyStore.ImportPrivateKey(wif, bs) - if err != nil { - return "", err + // The starting block for the key is the genesis block unless otherwise + // specified. + if bs == nil { + bs = &waddrmgr.BlockStamp{ + Hash: *activeNet.Params.GenesisHash, + Height: 0, + } } - // Immediately write wallet to disk. - w.KeyStore.MarkDirty() - if err := w.KeyStore.WriteIfDirty(); err != nil { - return "", fmt.Errorf("cannot write key: %v", err) + // Attempt to import private key into wallet. + addr, err := w.Manager.ImportPrivateKey(wif, bs) + if err != nil { + return "", err } // Rescan blockchain for transactions with txout scripts paying to the // imported address. if rescan { job := &RescanJob{ - Addrs: []btcutil.Address{addr}, - OutPoints: nil, - BlockStamp: keystore.BlockStamp{ - Hash: activeNet.Params.GenesisHash, - Height: 0, - }, + Addrs: []btcutil.Address{addr.Address()}, + OutPoints: nil, + BlockStamp: *bs, } // 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) } - addrStr := addr.EncodeAddress() + addrStr := addr.Address().EncodeAddress() log.Infof("Imported payment address %s", addrStr) // Return the payment address string of the imported private key. return addrStr, nil } -// ExportWatchingWallet returns the watching-only copy of a wallet. Both wallets -// share the same tx store, so locking one will lock the other as well. The -// returned wallet should be serialized and exported quickly, and then dropped -// from scope. -func (w *Wallet) ExportWatchingWallet() (*Wallet, error) { - ww, err := w.KeyStore.ExportWatchingWallet() +// ExportWatchingWallet returns a watching-only version of the wallet serialized +// in a map. +func (w *Wallet) ExportWatchingWallet() (map[string]string, error) { + tmpDir, err := ioutil.TempDir("", "btcwallet") if err != nil { return nil, err } + defer os.RemoveAll(tmpDir) - wa := *w - wa.KeyStore = ww - return &wa, nil + // Create a new file and write a copy of the current database into it. + woDbPath := filepath.Join(tmpDir, walletDbWatchingOnlyName) + 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. func (w *Wallet) exportBase64() (map[string]string, error) { - buf := bytes.Buffer{} + var buf bytes.Buffer m := make(map[string]string) - _, err := w.KeyStore.WriteTo(&buf) - if err != nil { + if err := w.db.Copy(&buf); err != nil { return nil, err } m["wallet"] = base64.StdEncoding.EncodeToString(buf.Bytes()) buf.Reset() - if _, err = w.TxStore.WriteTo(&buf); err != nil { + if _, err := w.TxStore.WriteTo(&buf); err != nil { return nil, err } m["tx"] = base64.StdEncoding.EncodeToString(buf.Bytes()) @@ -1180,114 +1183,62 @@ func (w *Wallet) ResendUnminedTxs() { // SortedActivePaymentAddresses returns a slice of all active payment // addresses in a wallet. -func (w *Wallet) SortedActivePaymentAddresses() []string { - infos := w.KeyStore.SortedActiveAddresses() - - addrs := make([]string, len(infos)) - for i, info := range infos { - addrs[i] = info.Address().EncodeAddress() +func (w *Wallet) SortedActivePaymentAddresses() ([]string, error) { + addrs, err := w.Manager.AllActiveAddresses() + if err != nil { + return nil, err } - 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) { - // Get current block's height and hash. - bs, err := w.SyncedChainTip() - if err != nil { - return nil, err - } - // Get next address from wallet. - addr, err := w.KeyStore.NextChainedAddress(bs) + account := uint32(0) + addrs, err := w.Manager.NextExternalAddresses(account, 1) 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. - 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 addr, nil + return utilAddrs[0], nil } // NewChangeAddress returns a new change address for a wallet. func (w *Wallet) NewChangeAddress() (btcutil.Address, error) { - // Get current block's height and hash. - bs, err := w.SyncedChainTip() + // Get next chained change address from wallet for account 0. + account := uint32(0) + addrs, err := w.Manager.NextInternalAddresses(account, 1) if err != nil { 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. - 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 addr, 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 + return utilAddrs[0], nil } // 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 } + +// 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 +} diff --git a/walletsetup.go b/walletsetup.go new file mode 100644 index 0000000..fca7aa5 --- /dev/null +++ b/walletsetup.go @@ -0,0 +1,296 @@ +/* + * Copyright (c) 2014 Conformal Systems LLC + * + * 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 +}