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.
// 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()

View file

@ -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

View file

@ -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 {

View file

@ -26,6 +26,7 @@ import (
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcwallet/legacy/keystore"
flags "github.com/btcsuite/go-flags"
)
@ -39,6 +40,8 @@ const (
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
}

View file

@ -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)
}

View file

@ -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

View file

@ -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))
// 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
}
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.

View file

@ -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 {
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
}
// The address lookup was successful which means there is further
// information about it available and it is "mine".
result.IsMine = true
result.Account = ""
if pka, ok := ainfo.(keystore.PubKeyAddress); ok {
result.IsCompressed = pka.Compressed()
result.PubKey = pka.ExportPubKey()
switch ma := ainfo.(type) {
case waddrmgr.ManagedPubKeyAddress:
result.IsCompressed = ma.Compressed()
result.PubKey = ma.ExportPubKey()
} else if sa, ok := ainfo.(keystore.ScriptAddress); ok {
case waddrmgr.ManagedScriptAddress:
result.IsScript = true
addresses := sa.Addresses()
addrStrings := make([]string, len(addresses))
for i, a := range addresses {
// 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
result.Hex = hex.EncodeToString(sa.Script())
class := sa.ScriptClass()
// script type
// Multi-signature scripts also provide the number of required
// signatures.
result.Script = class.String()
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),
[]byte(cmd.NewPassphrase))
if err == keystore.ErrWrongPassphrase {
if isManagerWrongPassphraseError(err) {
return nil, btcjson.ErrWalletPassphraseIncorrect
}
return nil, err

View file

@ -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
}

View file

@ -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

View file

@ -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()

View file

@ -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
}

519
wallet.go
View file

@ -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,7 +80,8 @@ func networkDir(chainParams *chaincfg.Params) string {
// addresses and keys),
type Wallet struct {
// Data stores
KeyStore *keystore.Store
db walletdb.DB
Manager *waddrmgr.Manager
TxStore *txstore.Store
chainSvr *chain.Client
@ -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.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},
Addrs: []btcutil.Address{addr.Address()},
OutPoints: nil,
BlockStamp: keystore.BlockStamp{
Hash: activeNet.Params.GenesisHash,
Height: 0,
},
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
}
}
// exportBase64 exports a wallet's serialized key, and tx stores as
// 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 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()
}
// NewAddress returns the next chained address for a wallet.
sort.Sort(sort.StringSlice(addrStrs))
return addrStrs, nil
}
// 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
}

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
}